@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/dist/backtest.js CHANGED
@@ -44,10 +44,10 @@ __reExport(backtest_exports, require("@tradejs/core/backtest"), module.exports);
44
44
 
45
45
  // src/testing.ts
46
46
  var import_indicators3 = require("@tradejs/core/indicators");
47
- var import_constants8 = require("@tradejs/core/constants");
47
+ var import_constants9 = require("@tradejs/core/constants");
48
48
  var import_strategies7 = require("@tradejs/core/strategies");
49
- var import_time2 = require("@tradejs/core/time");
50
- var import_logger13 = require("@tradejs/infra/logger");
49
+ var import_time3 = require("@tradejs/core/time");
50
+ var import_logger15 = require("@tradejs/infra/logger");
51
51
 
52
52
  // src/strategyRuntime.ts
53
53
  var import_constants5 = require("@tradejs/core/constants");
@@ -60,11 +60,8 @@ var import_constants3 = require("@tradejs/core/constants");
60
60
  var import_ml2 = require("@tradejs/infra/ml");
61
61
 
62
62
  // src/ai.ts
63
- var import_aiLanguages = require("@tradejs/core/aiLanguages");
64
- var import_aiEndpoints = require("@tradejs/core/aiEndpoints");
65
- var import_aiModels = require("@tradejs/core/aiModels");
63
+ var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
66
64
  var import_redis = require("@tradejs/infra/redis");
67
- var import_userSettings = require("@tradejs/infra/userSettings");
68
65
 
69
66
  // src/aiShared.ts
70
67
  var MAX_AI_SERIES_POINTS = 5;
@@ -1273,6 +1270,139 @@ var postProcessLocalAiAnalysisByStrategy = (signal, analysis, payload = buildAiP
1273
1270
  }) ?? strategyAnalysis;
1274
1271
  };
1275
1272
 
1273
+ // src/aiProvider.ts
1274
+ var import_aiEndpoints = require("@tradejs/core/aiEndpoints");
1275
+ var import_aiModels = require("@tradejs/core/aiModels");
1276
+ var import_aiLanguages = require("@tradejs/core/aiLanguages");
1277
+ var import_userSettings = require("@tradejs/infra/userSettings");
1278
+ var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
1279
+ var userSettingsCache = /* @__PURE__ */ new Map();
1280
+ var aiModelCache = /* @__PURE__ */ new Map();
1281
+ var normalizeResponseContent = (content) => {
1282
+ if (typeof content === "string") return content;
1283
+ if (content && typeof content === "object" && !Array.isArray(content)) {
1284
+ return content;
1285
+ }
1286
+ if (Array.isArray(content)) {
1287
+ return content.map(
1288
+ (part) => typeof part?.text === "string" ? part.text : ""
1289
+ ).join("\n").trim();
1290
+ }
1291
+ return String(content ?? "");
1292
+ };
1293
+ var getAiInvocationError = (error) => {
1294
+ const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
1295
+ const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
1296
+ details
1297
+ );
1298
+ const wrapped = new Error(
1299
+ isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
1300
+ );
1301
+ wrapped.cause = error;
1302
+ return wrapped;
1303
+ };
1304
+ var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
1305
+ var getAiModelCacheKey = (userName, modelName, temperature) => `${userName}::${modelName}::${temperature}`;
1306
+ var resolveAiModelName = (settings, requestedModelName) => {
1307
+ const explicitModelName = requestedModelName?.trim() ?? "";
1308
+ if (explicitModelName) return explicitModelName;
1309
+ return settings.AI_MODEL?.trim() || DEFAULT_AI_MODEL;
1310
+ };
1311
+ var getOpenRouterModelKwargs = (apiEndpoint) => {
1312
+ const endpoint = String(apiEndpoint ?? "").trim();
1313
+ if (!endpoint) return {};
1314
+ let hostname = "";
1315
+ try {
1316
+ hostname = new URL(endpoint).hostname;
1317
+ } catch {
1318
+ hostname = endpoint;
1319
+ }
1320
+ return hostname.toLowerCase().includes("openrouter") ? { provider: { ignore: ["azure"] } } : {};
1321
+ };
1322
+ var getAiUserSettings = async (userName = "root") => {
1323
+ let settingsPromise = userSettingsCache.get(userName);
1324
+ if (!settingsPromise) {
1325
+ settingsPromise = (0, import_userSettings.getUserSettings)(userName).then((settings2) => {
1326
+ const endpoint = (0, import_aiEndpoints.normalizeAiEndpoint)(settings2.AI_API_ENDPOINT);
1327
+ return {
1328
+ ...settings2,
1329
+ AI_API_ENDPOINT: endpoint,
1330
+ AI_MODEL: (0, import_aiModels.normalizeAiModel)(settings2.AI_MODEL, endpoint),
1331
+ AI_RESPONSE_LANGUAGE: (0, import_aiLanguages.normalizeAiResponseLanguage)(
1332
+ settings2.AI_RESPONSE_LANGUAGE
1333
+ )
1334
+ };
1335
+ });
1336
+ settingsPromise.catch(() => userSettingsCache.delete(userName));
1337
+ userSettingsCache.set(userName, settingsPromise);
1338
+ }
1339
+ const settings = await settingsPromise;
1340
+ if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
1341
+ throw new Error(`AI settings are incomplete for user ${userName}`);
1342
+ }
1343
+ return settings;
1344
+ };
1345
+ var getAiModel = async (userName = "root", requestedModelName, temperature = 0.2) => {
1346
+ const settings = await getAiUserSettings(userName);
1347
+ const modelName = resolveAiModelName(settings, requestedModelName);
1348
+ const cacheKey = getAiModelCacheKey(userName, modelName, temperature);
1349
+ let modelPromise = aiModelCache.get(cacheKey);
1350
+ if (!modelPromise) {
1351
+ modelPromise = import("@langchain/openai").then(({ ChatOpenAI }) => {
1352
+ const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
1353
+ return new ChatOpenAI({
1354
+ temperature,
1355
+ modelName,
1356
+ apiKey: settings.AI_API_KEY,
1357
+ ...Object.keys(modelKwargs).length ? { modelKwargs } : {},
1358
+ configuration: {
1359
+ baseURL: settings.AI_API_ENDPOINT,
1360
+ defaultHeaders: {
1361
+ "HTTP-Referer": "https://tradejs.dev",
1362
+ "X-Title": "Inv"
1363
+ }
1364
+ }
1365
+ });
1366
+ });
1367
+ modelPromise.catch(() => aiModelCache.delete(cacheKey));
1368
+ aiModelCache.set(cacheKey, modelPromise);
1369
+ }
1370
+ try {
1371
+ return await modelPromise;
1372
+ } catch (error) {
1373
+ aiModelCache.delete(cacheKey);
1374
+ userSettingsCache.delete(userName);
1375
+ throw error;
1376
+ }
1377
+ };
1378
+ var invokeAiChatWithUserMessageEncoding = async ({
1379
+ messages,
1380
+ userName = "root",
1381
+ model,
1382
+ temperature = 0.2
1383
+ }, resolveUserMessageEncoding) => {
1384
+ const [{ HumanMessage, SystemMessage }, aiModel] = await Promise.all([
1385
+ import("@langchain/core/messages"),
1386
+ getAiModel(userName, model, temperature)
1387
+ ]);
1388
+ const providerMessages = messages.map(
1389
+ (message) => message.role === "system" ? new SystemMessage(message.content) : new HumanMessage(
1390
+ resolveUserMessageEncoding(message) === "text-block" ? { content: [{ type: "text", text: message.content }] } : message.content
1391
+ )
1392
+ );
1393
+ try {
1394
+ const response = await aiModel.invoke(providerMessages);
1395
+ const content = normalizeResponseContent(response?.content);
1396
+ if (isEmptyResponseContent(content)) {
1397
+ throw new Error("AI provider returned an empty chat completion");
1398
+ }
1399
+ return { content };
1400
+ } catch (error) {
1401
+ throw getAiInvocationError(error);
1402
+ }
1403
+ };
1404
+ var invokeAiPromptChat = (options) => invokeAiChatWithUserMessageEncoding(options, () => "text-block");
1405
+
1276
1406
  // src/ai.ts
1277
1407
  var parseAIResponse = (input) => {
1278
1408
  try {
@@ -1286,18 +1416,6 @@ var parseAIResponse = (input) => {
1286
1416
  return {};
1287
1417
  }
1288
1418
  };
1289
- var normalizeResponseContent = (content) => {
1290
- if (typeof content === "string" || content && typeof content === "object") {
1291
- if (typeof content !== "object" || !Array.isArray(content)) {
1292
- return content;
1293
- }
1294
- }
1295
- if (Array.isArray(content)) {
1296
- const text = content.map((part) => typeof part?.text === "string" ? part.text : "").join("\n").trim();
1297
- return text;
1298
- }
1299
- return String(content ?? "");
1300
- };
1301
1419
  var normalizeAnalysis = (raw) => {
1302
1420
  const direction = raw?.direction === "LONG" || raw?.direction === "SHORT" ? raw.direction : null;
1303
1421
  const qualityNum = typeof raw?.quality === "number" ? Math.max(1, Math.min(5, Math.round(raw.quality))) : void 0;
@@ -1531,153 +1649,24 @@ Trade payload:
1531
1649
  ${JSON.stringify(payload)}
1532
1650
  ${buildAiHumanPromptAddonByStrategy(signal, payload)}
1533
1651
  `;
1534
- var getAiInvocationError = (error) => {
1535
- const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
1536
- const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
1537
- details
1538
- );
1539
- const wrapped = new Error(
1540
- isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
1541
- );
1542
- wrapped.cause = error;
1543
- return wrapped;
1544
- };
1545
- var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
1546
- var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
1547
- var userSettingsCache = /* @__PURE__ */ new Map();
1548
- var aiModelCache = /* @__PURE__ */ new Map();
1549
- var getAiModelCacheKey = (userName, modelName) => `${userName}::${modelName}`;
1550
- var resolveAiModelName = (settings, requestedModelName) => {
1551
- const explicitModelName = typeof requestedModelName === "string" ? requestedModelName.trim() : "";
1552
- if (explicitModelName) {
1553
- return explicitModelName;
1554
- }
1555
- const settingsModelName = typeof settings.AI_MODEL === "string" ? settings.AI_MODEL.trim() : "";
1556
- return settingsModelName || DEFAULT_AI_MODEL;
1557
- };
1558
- var getOpenRouterModelKwargs = (apiEndpoint) => {
1559
- const endpoint = String(apiEndpoint ?? "").trim();
1560
- if (!endpoint) {
1561
- return {};
1562
- }
1563
- let hostname = "";
1564
- try {
1565
- hostname = new URL(endpoint).hostname;
1566
- } catch {
1567
- hostname = endpoint;
1568
- }
1569
- if (!hostname.toLowerCase().includes("openrouter")) {
1570
- return {};
1571
- }
1572
- return {
1573
- provider: {
1574
- ignore: ["azure"]
1575
- }
1576
- };
1577
- };
1578
- var getAiSettings = async (userName = "root") => {
1579
- let settingsPromise = userSettingsCache.get(userName);
1580
- if (!settingsPromise) {
1581
- settingsPromise = (0, import_userSettings.getUserSettings)(userName).then((settings2) => {
1582
- const endpoint = (0, import_aiEndpoints.normalizeAiEndpoint)(settings2.AI_API_ENDPOINT);
1583
- return {
1584
- ...settings2,
1585
- AI_API_ENDPOINT: endpoint,
1586
- AI_MODEL: (0, import_aiModels.normalizeAiModel)(settings2.AI_MODEL, endpoint),
1587
- AI_RESPONSE_LANGUAGE: (0, import_aiLanguages.normalizeAiResponseLanguage)(
1588
- settings2.AI_RESPONSE_LANGUAGE
1589
- )
1590
- };
1591
- });
1592
- settingsPromise.catch(() => {
1593
- userSettingsCache.delete(userName);
1594
- });
1595
- userSettingsCache.set(userName, settingsPromise);
1596
- }
1597
- const settings = await settingsPromise;
1598
- if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
1599
- throw new Error(`AI settings are incomplete for user ${userName}`);
1600
- }
1601
- return settings;
1602
- };
1603
- var createAiModel = async (userName = "root", requestedModelName) => {
1604
- const settings = await getAiSettings(userName);
1605
- const modelName = resolveAiModelName(settings, requestedModelName);
1606
- const cacheKey = getAiModelCacheKey(userName, modelName);
1607
- let modelPromise = aiModelCache.get(cacheKey);
1608
- if (!modelPromise) {
1609
- modelPromise = (async () => {
1610
- const { ChatOpenAI } = await import("@langchain/openai");
1611
- const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
1612
- return new ChatOpenAI({
1613
- temperature: 0.2,
1614
- modelName,
1615
- apiKey: settings.AI_API_KEY,
1616
- ...Object.keys(modelKwargs).length ? { modelKwargs } : {},
1617
- configuration: {
1618
- baseURL: settings.AI_API_ENDPOINT,
1619
- defaultHeaders: {
1620
- "HTTP-Referer": "https://tradejs.dev",
1621
- "X-Title": "Inv"
1622
- }
1623
- }
1624
- });
1625
- })();
1626
- modelPromise.catch(() => {
1627
- aiModelCache.delete(cacheKey);
1628
- });
1629
- aiModelCache.set(cacheKey, modelPromise);
1630
- }
1631
- return modelPromise;
1632
- };
1633
- var getAiModel = async (userName = "root", requestedModelName) => {
1634
- const settings = await getAiSettings(userName);
1635
- const resolvedModelName = resolveAiModelName(settings, requestedModelName);
1636
- try {
1637
- return await createAiModel(userName, resolvedModelName);
1638
- } catch (error) {
1639
- aiModelCache.delete(getAiModelCacheKey(userName, resolvedModelName));
1640
- userSettingsCache.delete(userName);
1641
- throw error;
1642
- }
1643
- };
1644
1652
  var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
1645
- const [{ HumanMessage, SystemMessage }, model, settings] = await Promise.all([
1646
- import("@langchain/core/messages"),
1647
- getAiModel(options.userName, options.model),
1648
- getAiSettings(options.userName)
1649
- ]);
1650
- const messages = [];
1651
- const responseLanguage = (0, import_aiLanguages.getAiResponseLanguagePromptName)(
1652
- settings.AI_RESPONSE_LANGUAGE || import_aiLanguages.DEFAULT_AI_RESPONSE_LANGUAGE
1653
- );
1654
- messages.push(new SystemMessage(systemPrompt));
1655
- messages.push(
1656
- new SystemMessage(
1657
- `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
1658
- )
1659
- );
1660
- messages.push(
1661
- new HumanMessage({
1662
- content: [
1663
- {
1664
- type: "text",
1665
- text: humanPrompt
1666
- }
1667
- ]
1668
- })
1653
+ const settings = await getAiUserSettings(options.userName);
1654
+ const responseLanguage = (0, import_aiLanguages2.getAiResponseLanguagePromptName)(
1655
+ settings.AI_RESPONSE_LANGUAGE || import_aiLanguages2.DEFAULT_AI_RESPONSE_LANGUAGE
1669
1656
  );
1670
- let response;
1671
- try {
1672
- response = await model.invoke(messages);
1673
- } catch (error) {
1674
- throw getAiInvocationError(error);
1675
- }
1676
- const responseContent = normalizeResponseContent(response?.content);
1677
- if (isEmptyResponseContent(responseContent)) {
1678
- throw new Error("AI provider returned an empty chat completion");
1679
- }
1680
- const parsed = parseAIResponse(responseContent);
1657
+ const response = await invokeAiPromptChat({
1658
+ userName: options.userName,
1659
+ model: options.model,
1660
+ messages: [
1661
+ { role: "system", content: systemPrompt },
1662
+ {
1663
+ role: "system",
1664
+ content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
1665
+ },
1666
+ { role: "user", content: humanPrompt }
1667
+ ]
1668
+ });
1669
+ const parsed = parseAIResponse(response.content);
1681
1670
  const normalized = normalizeAnalysis(parsed);
1682
1671
  if (!options.signal) {
1683
1672
  return normalized;
@@ -6241,7 +6230,323 @@ var createStrategyRuntime = ({
6241
6230
  setStrategyRuntimeFactory(createStrategyRuntime);
6242
6231
 
6243
6232
  // src/connectorsRegistry.ts
6233
+ var import_logger14 = require("@tradejs/infra/logger");
6234
+
6235
+ // src/connectorRuntime.ts
6236
+ var import_logger13 = require("@tradejs/infra/logger");
6237
+ var import_tradingAccounts = require("@tradejs/infra/tradingAccounts");
6238
+
6239
+ // src/timescaleCachedKline.ts
6240
+ var import_constants6 = require("@tradejs/core/constants");
6241
+ var import_data4 = require("@tradejs/core/data");
6242
+ var import_time2 = require("@tradejs/core/time");
6243
+ var import_async = require("@tradejs/core/async");
6244
6244
  var import_logger12 = require("@tradejs/infra/logger");
6245
+ var import_candles = require("@tradejs/infra/timescale/candles");
6246
+ var DEFAULT_LIMIT = 1e3;
6247
+ var DEFAULT_CACHE_FALLBACK_WINDOW = 1e3;
6248
+ var DEFAULT_TIMESCALE_RETRIES = 2;
6249
+ var DEFAULT_TIMESCALE_RETRY_DELAY_MS = 1e3;
6250
+ var resolveNonNegativeInt = (value, fallback) => {
6251
+ const parsed = Number(value);
6252
+ if (!Number.isFinite(parsed) || parsed < 0) {
6253
+ return fallback;
6254
+ }
6255
+ return Math.floor(parsed);
6256
+ };
6257
+ var getTimescaleRetryCount = () => resolveNonNegativeInt(
6258
+ process.env.TIMESCALE_KLINE_RETRIES,
6259
+ DEFAULT_TIMESCALE_RETRIES
6260
+ );
6261
+ var getTimescaleRetryDelayMs = () => resolveNonNegativeInt(
6262
+ process.env.TIMESCALE_KLINE_RETRY_DELAY_MS,
6263
+ DEFAULT_TIMESCALE_RETRY_DELAY_MS
6264
+ );
6265
+ var intervalMsOf = (interval) => interval * 6e4;
6266
+ var clampToClosedCandle = (value, intervalMs) => Math.floor(value / intervalMs) * intervalMs;
6267
+ var normalizeRangeToClosed = (intervalMs, start, end) => {
6268
+ const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
6269
+ const normStart = start !== void 0 ? clampToClosedCandle(start, intervalMs) : 0;
6270
+ const cappedEnd = Math.min(end ?? Date.now(), lastClosed);
6271
+ const normEnd = clampToClosedCandle(cappedEnd, intervalMs);
6272
+ return { normStart, normEnd };
6273
+ };
6274
+ var rowsToKline = (rows) => rows.map(({ ts, ...data }) => ({
6275
+ timestamp: new Date(ts).getTime(),
6276
+ ...data
6277
+ }));
6278
+ var createTimescaleCachedKline = ({
6279
+ provider,
6280
+ request,
6281
+ intervalToMinutes,
6282
+ limit = DEFAULT_LIMIT,
6283
+ cacheFallbackWindow = DEFAULT_CACHE_FALLBACK_WINDOW
6284
+ }) => {
6285
+ let isTimescaleFallbackMode = false;
6286
+ const runTimescaleOperation = async (operation) => {
6287
+ const retries = getTimescaleRetryCount();
6288
+ const retryDelayMs = getTimescaleRetryDelayMs();
6289
+ for (let attempt = 0; ; attempt += 1) {
6290
+ try {
6291
+ return await operation();
6292
+ } catch (error) {
6293
+ if (attempt >= retries) {
6294
+ throw error;
6295
+ }
6296
+ const waitMs = retryDelayMs * 2 ** attempt;
6297
+ if (waitMs > 0) {
6298
+ await (0, import_async.delay)(waitMs);
6299
+ }
6300
+ }
6301
+ }
6302
+ };
6303
+ const loadData = async (direction, pointer, limitBoundary, requestParams, intervalMs, options = {}) => {
6304
+ if (pointer === void 0) return { data: [], loaded: false };
6305
+ let accumulated = [];
6306
+ let fulfilled = false;
6307
+ let loaded = false;
6308
+ const shouldAccumulate = options.accumulate ?? true;
6309
+ while (!fulfilled) {
6310
+ const currentPointer = pointer;
6311
+ const params = {
6312
+ symbol: requestParams.symbol,
6313
+ interval: requestParams.interval,
6314
+ silent: requestParams.silent,
6315
+ end: requestParams.end
6316
+ };
6317
+ if (direction === "older") {
6318
+ params.end = pointer;
6319
+ if (limitBoundary !== void 0) params.start = limitBoundary;
6320
+ } else {
6321
+ params.start = pointer;
6322
+ if (limitBoundary !== void 0) params.end = limitBoundary;
6323
+ }
6324
+ const partData = await request(params);
6325
+ if (!partData.length) {
6326
+ fulfilled = true;
6327
+ break;
6328
+ }
6329
+ loaded = true;
6330
+ if (options.onPartData) {
6331
+ await options.onPartData(partData);
6332
+ }
6333
+ if (shouldAccumulate) {
6334
+ accumulated = direction === "older" ? (0, import_data4.mergeData)(partData, accumulated) : (0, import_data4.mergeData)(accumulated, partData);
6335
+ }
6336
+ const boundaryReached = limitBoundary !== void 0 && (direction === "older" && currentPointer <= limitBoundary || direction === "newer" && currentPointer >= limitBoundary);
6337
+ if (partData.length < limit || boundaryReached) {
6338
+ fulfilled = true;
6339
+ break;
6340
+ }
6341
+ const nextPointer = direction === "older" ? (0, import_time2.getItemTimestamp)(partData[0]) - intervalMs : (0, import_time2.getItemTimestamp)(partData[partData.length - 1]) + intervalMs;
6342
+ if (!Number.isFinite(nextPointer) || nextPointer === currentPointer) {
6343
+ fulfilled = true;
6344
+ break;
6345
+ }
6346
+ pointer = nextPointer;
6347
+ }
6348
+ return { data: accumulated, loaded };
6349
+ };
6350
+ const refreshTail = async ({
6351
+ symbol,
6352
+ interval,
6353
+ silent,
6354
+ tailCount = 2
6355
+ }) => {
6356
+ const intMinutes = intervalToMinutes(interval);
6357
+ if (!intMinutes) {
6358
+ import_logger12.logger.log("error", "refreshTail: invalid interval %s", interval);
6359
+ return;
6360
+ }
6361
+ const intervalMs = intervalMsOf(intMinutes);
6362
+ const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
6363
+ const tailEnd = lastClosed + intervalMs;
6364
+ const tailStart = tailEnd - tailCount * intervalMs;
6365
+ const part = await request({
6366
+ symbol,
6367
+ interval,
6368
+ start: tailStart,
6369
+ end: tailEnd,
6370
+ silent
6371
+ });
6372
+ if (part.length) {
6373
+ await runTimescaleOperation(
6374
+ () => (0, import_candles.upsertCandles)((0, import_candles.toRows)(provider, symbol, intMinutes, part))
6375
+ );
6376
+ }
6377
+ };
6378
+ return async ({
6379
+ symbol,
6380
+ interval,
6381
+ start: defaultStart,
6382
+ end: defaultEnd,
6383
+ silent = false,
6384
+ cacheOnly = false,
6385
+ warmOnly = false
6386
+ }) => {
6387
+ const intMinutes = intervalToMinutes(interval);
6388
+ if (!intMinutes) {
6389
+ import_logger12.logger.log("error", "kline: invalid interval %s", interval);
6390
+ return [];
6391
+ }
6392
+ if (defaultStart !== void 0 && defaultEnd !== void 0 && defaultEnd <= defaultStart) {
6393
+ return [];
6394
+ }
6395
+ const intervalMs = intervalMsOf(intMinutes);
6396
+ try {
6397
+ const edges = await runTimescaleOperation(
6398
+ () => (0, import_candles.getDataEdges)(provider, symbol, intMinutes)
6399
+ );
6400
+ let dataStart = edges.min;
6401
+ let dataEnd = edges.max;
6402
+ const { normStart, normEnd } = normalizeRangeToClosed(
6403
+ intervalMs,
6404
+ defaultStart,
6405
+ defaultEnd
6406
+ );
6407
+ if (cacheOnly) {
6408
+ const base = edges.max ?? Date.now();
6409
+ const s = Math.max(
6410
+ defaultStart ?? base - cacheFallbackWindow * intervalMs,
6411
+ 0
6412
+ );
6413
+ const e = defaultEnd ?? base;
6414
+ const dbData2 = await runTimescaleOperation(
6415
+ () => (0, import_candles.getCandlesRange)(provider, symbol, intMinutes, s, e)
6416
+ );
6417
+ return rowsToKline(dbData2);
6418
+ }
6419
+ const needOlderData = defaultStart !== void 0 && (dataStart === void 0 || normStart < dataStart);
6420
+ const persistPartData = warmOnly ? (partData) => runTimescaleOperation(
6421
+ () => (0, import_candles.upsertCandles)((0, import_candles.toRows)(provider, symbol, intMinutes, partData))
6422
+ ) : void 0;
6423
+ if (needOlderData) {
6424
+ const pointerForOlder = dataStart ?? normEnd ?? Date.now();
6425
+ const olderResult = await loadData(
6426
+ "older",
6427
+ pointerForOlder,
6428
+ normStart,
6429
+ {
6430
+ symbol,
6431
+ interval,
6432
+ silent,
6433
+ start: normStart,
6434
+ end: pointerForOlder
6435
+ },
6436
+ intervalMs,
6437
+ {
6438
+ accumulate: !warmOnly,
6439
+ onPartData: persistPartData
6440
+ }
6441
+ );
6442
+ if (warmOnly && olderResult.loaded) {
6443
+ dataStart = normStart;
6444
+ if (dataEnd === void 0) {
6445
+ dataEnd = normEnd;
6446
+ }
6447
+ } else if (olderResult.data.length) {
6448
+ await runTimescaleOperation(
6449
+ () => (0, import_candles.upsertCandles)(
6450
+ (0, import_candles.toRows)(provider, symbol, intMinutes, olderResult.data)
6451
+ )
6452
+ );
6453
+ dataStart = normStart;
6454
+ if (dataEnd === void 0) {
6455
+ dataEnd = normEnd;
6456
+ }
6457
+ }
6458
+ }
6459
+ const needNewerData = defaultEnd !== void 0 && (dataEnd === void 0 || normEnd > dataEnd);
6460
+ if (needNewerData) {
6461
+ const fallbackStart = (0, import_time2.getTimestamp)(import_constants6.PRELOAD_FALLBACK_DAYS);
6462
+ const pointerForNewer = dataEnd ?? (defaultStart !== void 0 ? normStart : fallbackStart) ?? 0;
6463
+ const newerResult = await loadData(
6464
+ "newer",
6465
+ pointerForNewer,
6466
+ normEnd,
6467
+ {
6468
+ symbol,
6469
+ interval,
6470
+ silent,
6471
+ start: pointerForNewer,
6472
+ end: normEnd
6473
+ },
6474
+ intervalMs,
6475
+ {
6476
+ accumulate: !warmOnly,
6477
+ onPartData: persistPartData
6478
+ }
6479
+ );
6480
+ if (warmOnly && newerResult.loaded) {
6481
+ dataEnd = normEnd;
6482
+ } else if (newerResult.data.length) {
6483
+ await runTimescaleOperation(
6484
+ () => (0, import_candles.upsertCandles)(
6485
+ (0, import_candles.toRows)(provider, symbol, intMinutes, newerResult.data)
6486
+ )
6487
+ );
6488
+ dataEnd = normEnd;
6489
+ }
6490
+ }
6491
+ const isRightEdgeQuery = defaultEnd === void 0 || defaultEnd && defaultEnd >= Date.now() - intervalMs;
6492
+ if (!cacheOnly && isRightEdgeQuery) {
6493
+ await refreshTail({ symbol, interval, silent });
6494
+ }
6495
+ if (warmOnly) {
6496
+ if (isTimescaleFallbackMode) {
6497
+ isTimescaleFallbackMode = false;
6498
+ import_logger12.logger.log("info", "TimescaleDB connection restored for kline cache");
6499
+ }
6500
+ return [];
6501
+ }
6502
+ const rangeStart = defaultStart ?? dataStart ?? 0;
6503
+ const rangeEnd = defaultEnd ?? dataEnd ?? Date.now();
6504
+ const { normStart: finalStart, normEnd: finalEnd } = normalizeRangeToClosed(intervalMs, rangeStart, rangeEnd);
6505
+ const dbData = await runTimescaleOperation(
6506
+ () => (0, import_candles.getCandlesRange)(provider, symbol, intMinutes, finalStart, finalEnd)
6507
+ );
6508
+ if (isTimescaleFallbackMode) {
6509
+ isTimescaleFallbackMode = false;
6510
+ import_logger12.logger.log("info", "TimescaleDB connection restored for kline cache");
6511
+ }
6512
+ return rowsToKline(dbData);
6513
+ } catch (error) {
6514
+ if (!isTimescaleFallbackMode) {
6515
+ isTimescaleFallbackMode = true;
6516
+ import_logger12.logger.log(
6517
+ "warn",
6518
+ "TimescaleDB unavailable for %s %s: %s. Falling back to exchange API.",
6519
+ symbol,
6520
+ interval,
6521
+ String(error)
6522
+ );
6523
+ }
6524
+ if (cacheOnly || warmOnly) {
6525
+ return [];
6526
+ }
6527
+ return request({
6528
+ symbol,
6529
+ interval,
6530
+ start: defaultStart,
6531
+ end: defaultEnd,
6532
+ silent
6533
+ });
6534
+ }
6535
+ };
6536
+ };
6537
+
6538
+ // src/connectorRuntime.ts
6539
+ var connectorRuntime = {
6540
+ logger: import_logger13.logger,
6541
+ resolveTradingAccount: import_tradingAccounts.resolveTradingAccount,
6542
+ createCachedKline: createTimescaleCachedKline
6543
+ };
6544
+ var bindConnectorRuntime = (creator, runtime = connectorRuntime) => {
6545
+ const boundCreator = (config) => creator(config, runtime);
6546
+ return boundCreator;
6547
+ };
6548
+
6549
+ // src/connectorsRegistry.ts
6245
6550
  var createConnectorRegistryState = () => ({
6246
6551
  connectorCreators: /* @__PURE__ */ new Map(),
6247
6552
  providerToConnectorName: /* @__PURE__ */ new Map(),
@@ -6293,7 +6598,7 @@ var normalizeProviders = (providers, connectorName) => {
6293
6598
  var registerProvider = (provider, connectorName, source, providerToConnectorName) => {
6294
6599
  const existing = providerToConnectorName.get(provider);
6295
6600
  if (existing && existing !== connectorName) {
6296
- import_logger12.logger.warn(
6601
+ import_logger14.logger.warn(
6297
6602
  'Skip duplicate connector provider "%s" from %s: already mapped to %s',
6298
6603
  provider,
6299
6604
  source,
@@ -6306,11 +6611,11 @@ var registerProvider = (provider, connectorName, source, providerToConnectorName
6306
6611
  var registerEntry = (entry, source, state) => {
6307
6612
  const connectorName = String(entry?.name ?? "").trim();
6308
6613
  if (!connectorName) {
6309
- import_logger12.logger.warn("Skip connector entry without name from %s", source);
6614
+ import_logger14.logger.warn("Skip connector entry without name from %s", source);
6310
6615
  return;
6311
6616
  }
6312
6617
  if (typeof entry.creator !== "function") {
6313
- import_logger12.logger.warn(
6618
+ import_logger14.logger.warn(
6314
6619
  'Skip connector entry "%s" from %s: creator must be a function',
6315
6620
  connectorName,
6316
6621
  source
@@ -6322,7 +6627,7 @@ var registerEntry = (entry, source, state) => {
6322
6627
  state.connectorCreators
6323
6628
  );
6324
6629
  if (existingByName) {
6325
- import_logger12.logger.warn(
6630
+ import_logger14.logger.warn(
6326
6631
  'Skip duplicate connector "%s" from %s: already registered as %s',
6327
6632
  connectorName,
6328
6633
  source,
@@ -6330,7 +6635,10 @@ var registerEntry = (entry, source, state) => {
6330
6635
  );
6331
6636
  return;
6332
6637
  }
6333
- state.connectorCreators.set(connectorName, entry.creator);
6638
+ state.connectorCreators.set(
6639
+ connectorName,
6640
+ bindConnectorRuntime(entry.creator)
6641
+ );
6334
6642
  const providers = normalizeProviders(entry.providers, connectorName);
6335
6643
  for (const provider of providers) {
6336
6644
  registerProvider(
@@ -6394,7 +6702,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
6394
6702
  );
6395
6703
  const pluginDefinition = extractConnectorPluginDefinition(moduleExport);
6396
6704
  if (!pluginDefinition) {
6397
- import_logger12.logger.warn(
6705
+ import_logger14.logger.warn(
6398
6706
  'Skip connector plugin "%s": export { connectorEntries } is missing',
6399
6707
  moduleName
6400
6708
  );
@@ -6402,7 +6710,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
6402
6710
  }
6403
6711
  registerEntries2(pluginDefinition.connectorEntries, moduleName, state);
6404
6712
  } catch (error) {
6405
- import_logger12.logger.warn(
6713
+ import_logger14.logger.warn(
6406
6714
  'Failed to load connector plugin "%s": %s',
6407
6715
  moduleName,
6408
6716
  String(error)
@@ -6547,7 +6855,7 @@ var import_coreResearch = require("@tradejs/infra/coreResearch");
6547
6855
  var import_ml3 = require("@tradejs/infra/ml");
6548
6856
 
6549
6857
  // src/executionCosts.ts
6550
- var import_constants6 = require("@tradejs/core/constants");
6858
+ var import_constants7 = require("@tradejs/core/constants");
6551
6859
  var finiteOr = (value, fallback) => {
6552
6860
  const parsed = Number(value);
6553
6861
  return Number.isFinite(parsed) ? parsed : fallback;
@@ -6587,8 +6895,8 @@ var resolveExecutionCosts = async (params) => {
6587
6895
  const cacheOnly = config.EXECUTION_COSTS_CACHE_ONLY === true;
6588
6896
  const hasConfiguredFees = Number.isFinite(Number(config.MAKER_FEE_RATE)) && Number.isFinite(Number(config.TAKER_FEE_RATE));
6589
6897
  const exchangeFees = !cacheOnly && !hasConfiguredFees && connector.getTradingFeeRate ? await loadTradingFee(connector, symbol).catch(() => null) : null;
6590
- const makerRate = hasConfiguredFees ? Number(config.MAKER_FEE_RATE) : exchangeFees?.makerRate ?? import_constants6.FEE_PERCENT;
6591
- const takerRate = hasConfiguredFees ? Number(config.TAKER_FEE_RATE) : exchangeFees?.takerRate ?? import_constants6.FEE_PERCENT;
6898
+ const makerRate = hasConfiguredFees ? Number(config.MAKER_FEE_RATE) : exchangeFees?.makerRate ?? import_constants7.FEE_PERCENT;
6899
+ const takerRate = hasConfiguredFees ? Number(config.TAKER_FEE_RATE) : exchangeFees?.takerRate ?? import_constants7.FEE_PERCENT;
6592
6900
  const fundingEnabled = config.FUNDING_ENABLED !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
6593
6901
  const fundingRates = fundingEnabled ? await loadFundingRates(connector, symbol, startTime, endTime) : [];
6594
6902
  const requestedLeverage = Math.max(1, finiteOr(config.LEVERAGE, 10));
@@ -6609,18 +6917,18 @@ var resolveExecutionCosts = async (params) => {
6609
6917
  toTimestamp: fundingRates.at(-1)?.timestamp ?? null
6610
6918
  },
6611
6919
  slippage: {
6612
- baseBps: finiteOr(config.SLIPPAGE_BASE_BPS, import_constants6.BACKTEST_BASE_SLIPPAGE_BPS),
6920
+ baseBps: finiteOr(config.SLIPPAGE_BASE_BPS, import_constants7.BACKTEST_BASE_SLIPPAGE_BPS),
6613
6921
  spreadMultiplier: finiteOr(
6614
6922
  config.SLIPPAGE_SPREAD_MULTIPLIER,
6615
- import_constants6.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER
6923
+ import_constants7.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER
6616
6924
  ),
6617
6925
  marketImpactBps: finiteOr(
6618
6926
  config.SLIPPAGE_MARKET_IMPACT_BPS,
6619
- import_constants6.BACKTEST_MARKET_IMPACT_BPS
6927
+ import_constants7.BACKTEST_MARKET_IMPACT_BPS
6620
6928
  ),
6621
6929
  delayRiskMultiplier: finiteOr(
6622
6930
  config.SLIPPAGE_DELAY_RISK_MULTIPLIER,
6623
- import_constants6.BACKTEST_DELAY_RISK_MULTIPLIER
6931
+ import_constants7.BACKTEST_DELAY_RISK_MULTIPLIER
6624
6932
  ),
6625
6933
  source: config.SLIPPAGE_BASE_BPS != null || config.SLIPPAGE_SPREAD_MULTIPLIER != null || config.SLIPPAGE_MARKET_IMPACT_BPS != null ? "config" : "fallback"
6626
6934
  },
@@ -6638,7 +6946,7 @@ var resolveExecutionCosts = async (params) => {
6638
6946
 
6639
6947
  // src/testConnector.ts
6640
6948
  var import_node_crypto4 = require("crypto");
6641
- var import_constants7 = require("@tradejs/core/constants");
6949
+ var import_constants8 = require("@tradejs/core/constants");
6642
6950
  var import_backtest = require("@tradejs/core/backtest");
6643
6951
  var import_trade2 = require("@tradejs/core/trade");
6644
6952
  var import_math = require("@tradejs/core/math");
@@ -6665,14 +6973,14 @@ var createTestConnector = (connector, context) => {
6665
6973
  const positionLog = [];
6666
6974
  const fastMode = Boolean(context?.fastMode);
6667
6975
  const executionCostModel = context?.executionCostModel;
6668
- const makerFeeRate = executionCostModel?.fees.makerRate ?? import_constants7.FEE_PERCENT;
6669
- const takerFeeRate = executionCostModel?.fees.takerRate ?? import_constants7.FEE_PERCENT;
6976
+ const makerFeeRate = executionCostModel?.fees.makerRate ?? import_constants8.FEE_PERCENT;
6977
+ const takerFeeRate = executionCostModel?.fees.takerRate ?? import_constants8.FEE_PERCENT;
6670
6978
  const fundingRates = [...context?.fundingRates ?? []].sort(
6671
6979
  (left, right) => left.timestamp - right.timestamp
6672
6980
  );
6673
6981
  const processedFundingTimestamps = /* @__PURE__ */ new Set();
6674
6982
  let currentPosition = null;
6675
- let amount = import_constants7.INITIAL_BACKTEST_AMOUNT;
6983
+ let amount = import_constants8.INITIAL_BACKTEST_AMOUNT;
6676
6984
  let originalQty = 0;
6677
6985
  let currentPositionProfit = 0;
6678
6986
  let currentSignalId = null;
@@ -7115,7 +7423,7 @@ var createTestConnector = (connector, context) => {
7115
7423
  profit: fullStat.netProfit
7116
7424
  } : {
7117
7425
  amount: (0, import_math.round)(amount),
7118
- profit: (0, import_math.round)(amount - import_constants7.INITIAL_BACKTEST_AMOUNT),
7426
+ profit: (0, import_math.round)(amount - import_constants8.INITIAL_BACKTEST_AMOUNT),
7119
7427
  orders: positionLog.length
7120
7428
  },
7121
7429
  orderLogId,
@@ -8143,7 +8451,7 @@ var BACKTEST_INTERVAL = "15";
8143
8451
  var resolveBacktestExecutionInterval = (interval) => {
8144
8452
  const normalized = String(interval);
8145
8453
  if (normalized === "15") {
8146
- return import_constants8.BACKTEST_EXECUTION_INTERVAL;
8454
+ return import_constants9.BACKTEST_EXECUTION_INTERVAL;
8147
8455
  }
8148
8456
  if (normalized === "60") {
8149
8457
  return "15";
@@ -8190,7 +8498,7 @@ var filterClosedAlignedCandles = (data, btcData, interval) => {
8190
8498
  };
8191
8499
  };
8192
8500
  var shouldLoadBacktestExecutionCandles = (interval) => {
8193
- if (!import_constants8.BACKTEST_LOWER_TIMEFRAME_EXECUTION_ENABLED) {
8501
+ if (!import_constants9.BACKTEST_LOWER_TIMEFRAME_EXECUTION_ENABLED) {
8194
8502
  return false;
8195
8503
  }
8196
8504
  const executionInterval = resolveBacktestExecutionInterval(interval);
@@ -8300,7 +8608,7 @@ var prepareTestingData = async (params) => {
8300
8608
  throw new Error(`Unknown connector: ${connectorName}`);
8301
8609
  }
8302
8610
  if (!binanceConnector || !coinbaseConnector) {
8303
- import_logger13.logger.warn(
8611
+ import_logger15.logger.warn(
8304
8612
  "Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
8305
8613
  connectorName
8306
8614
  );
@@ -8583,7 +8891,7 @@ var testing = async (test) => {
8583
8891
  if (!start) {
8584
8892
  throw new Error("no start");
8585
8893
  }
8586
- const preloadStart = (0, import_time2.getBacktestPreloadStart)(start);
8894
+ const preloadStart = (0, import_time3.getBacktestPreloadStart)(start);
8587
8895
  const progress = createBacktestProgress({
8588
8896
  testName: name,
8589
8897
  symbol,
@@ -8683,7 +8991,7 @@ var testingGroupInSharedCandleLoop = async (tests) => {
8683
8991
  if (!start) {
8684
8992
  throw new Error("no start");
8685
8993
  }
8686
- const preloadStart = (0, import_time2.getBacktestPreloadStart)(start);
8994
+ const preloadStart = (0, import_time3.getBacktestPreloadStart)(start);
8687
8995
  const progress = createBacktestProgress({
8688
8996
  testName: first.name,
8689
8997
  symbol,