@tradejs/node 1.0.6 → 1.0.8
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/ai.js +274 -126
- package/dist/ai.mjs +3 -3
- package/dist/backtest.js +345 -42
- package/dist/backtest.mjs +98 -16
- package/dist/chunk-2JKX3DM7.mjs +619 -0
- package/dist/{chunk-XW5L327F.mjs → chunk-JMDYEKIO.mjs} +1 -1
- package/dist/chunk-JRRG3YQG.mjs +154 -0
- package/dist/{chunk-CGJ2UU6H.mjs → chunk-JU77QVJ3.mjs} +8 -1
- package/dist/{chunk-SCMBUEGK.mjs → chunk-KMJQQ53K.mjs} +1 -1
- package/dist/{chunk-GKDBAF3A.mjs → chunk-KZDHZ56N.mjs} +25 -7
- package/dist/{chunk-EOZSJKUM.mjs → chunk-WGOYR6AB.mjs} +1 -1
- package/dist/cli.d.mts +20 -2
- package/dist/cli.d.ts +20 -2
- package/dist/cli.js +456 -183
- package/dist/cli.mjs +155 -49
- package/dist/connectors.js +6 -1
- package/dist/connectors.mjs +2 -2
- package/dist/constants.js +1 -1
- package/dist/constants.mjs +1 -1
- package/dist/registry.js +6 -1
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +24 -9
- package/dist/strategies.d.ts +24 -9
- package/dist/strategies.js +7465 -6467
- package/dist/strategies.mjs +865 -140
- package/package.json +6 -6
- package/dist/chunk-72FKXJ2I.mjs +0 -473
- package/dist/chunk-PQH5PAFC.mjs +0 -49
package/dist/cli.js
CHANGED
|
@@ -36,9 +36,9 @@ var require_lodash = __commonJS({
|
|
|
36
36
|
"use strict";
|
|
37
37
|
(function() {
|
|
38
38
|
var undefined2;
|
|
39
|
-
var VERSION = "4.
|
|
39
|
+
var VERSION = "4.18.1";
|
|
40
40
|
var LARGE_ARRAY_SIZE = 200;
|
|
41
|
-
var CORE_ERROR_TEXT = "Unsupported core-js use. Try https://npms.io/search?q=ponyfill.", FUNC_ERROR_TEXT = "Expected a function", INVALID_TEMPL_VAR_ERROR_TEXT = "Invalid `variable` option passed into `_.template`";
|
|
41
|
+
var CORE_ERROR_TEXT = "Unsupported core-js use. Try https://npms.io/search?q=ponyfill.", FUNC_ERROR_TEXT = "Expected a function", INVALID_TEMPL_VAR_ERROR_TEXT = "Invalid `variable` option passed into `_.template`", INVALID_TEMPL_IMPORTS_ERROR_TEXT = "Invalid `imports` option passed into `_.template`";
|
|
42
42
|
var HASH_UNDEFINED = "__lodash_hash_undefined__";
|
|
43
43
|
var MAX_MEMOIZE_SIZE = 500;
|
|
44
44
|
var PLACEHOLDER = "__lodash_placeholder__";
|
|
@@ -1964,8 +1964,21 @@ var require_lodash = __commonJS({
|
|
|
1964
1964
|
}
|
|
1965
1965
|
function baseUnset(object, path3) {
|
|
1966
1966
|
path3 = castPath(path3, object);
|
|
1967
|
-
|
|
1968
|
-
|
|
1967
|
+
var index = -1, length = path3.length;
|
|
1968
|
+
if (!length) {
|
|
1969
|
+
return true;
|
|
1970
|
+
}
|
|
1971
|
+
while (++index < length) {
|
|
1972
|
+
var key = toKey(path3[index]);
|
|
1973
|
+
if (key === "__proto__" && !hasOwnProperty.call(object, "__proto__")) {
|
|
1974
|
+
return false;
|
|
1975
|
+
}
|
|
1976
|
+
if ((key === "constructor" || key === "prototype") && index < length - 1) {
|
|
1977
|
+
return false;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
var obj = parent(object, path3);
|
|
1981
|
+
return obj == null || delete obj[toKey(last(path3))];
|
|
1969
1982
|
}
|
|
1970
1983
|
function baseUpdate(object, path3, updater, customizer) {
|
|
1971
1984
|
return baseSet(object, path3, updater(baseGet(object, path3)), customizer);
|
|
@@ -3292,7 +3305,7 @@ var require_lodash = __commonJS({
|
|
|
3292
3305
|
var index = -1, length = pairs == null ? 0 : pairs.length, result2 = {};
|
|
3293
3306
|
while (++index < length) {
|
|
3294
3307
|
var pair = pairs[index];
|
|
3295
|
-
result2
|
|
3308
|
+
baseAssignValue(result2, pair[0], pair[1]);
|
|
3296
3309
|
}
|
|
3297
3310
|
return result2;
|
|
3298
3311
|
}
|
|
@@ -4676,8 +4689,13 @@ var require_lodash = __commonJS({
|
|
|
4676
4689
|
options = undefined2;
|
|
4677
4690
|
}
|
|
4678
4691
|
string = toString(string);
|
|
4679
|
-
options =
|
|
4680
|
-
var imports =
|
|
4692
|
+
options = assignWith({}, options, settings, customDefaultsAssignIn);
|
|
4693
|
+
var imports = assignWith({}, options.imports, settings.imports, customDefaultsAssignIn), importsKeys = keys(imports), importsValues = baseValues(imports, importsKeys);
|
|
4694
|
+
arrayEach(importsKeys, function(key) {
|
|
4695
|
+
if (reForbiddenIdentifierChars.test(key)) {
|
|
4696
|
+
throw new Error2(INVALID_TEMPL_IMPORTS_ERROR_TEXT);
|
|
4697
|
+
}
|
|
4698
|
+
});
|
|
4681
4699
|
var isEscaping, isEvaluating, index = 0, interpolate = options.interpolate || reNoMatch, source = "__p += '";
|
|
4682
4700
|
var reDelimiters = RegExp2(
|
|
4683
4701
|
(options.escape || reNoMatch).source + "|" + interpolate.source + "|" + (interpolate === reInterpolate ? reEsTemplate : reNoMatch).source + "|" + (options.evaluate || reNoMatch).source + "|$",
|
|
@@ -5518,7 +5536,9 @@ __export(cli_exports, {
|
|
|
5518
5536
|
cleanRedis: () => cleanRedis,
|
|
5519
5537
|
drawStatInCLI: () => drawStatInCLI,
|
|
5520
5538
|
getTickers: () => getTickers,
|
|
5539
|
+
loadTradejsConfig: () => loadTradejsConfig,
|
|
5521
5540
|
makeScreenshots: () => makeScreenshots,
|
|
5541
|
+
sendTextToTG: () => sendTextToTG,
|
|
5522
5542
|
sendToAI: () => sendToAI,
|
|
5523
5543
|
sendToTG: () => sendToTG,
|
|
5524
5544
|
update: () => update
|
|
@@ -5541,7 +5561,7 @@ var getPositiveIntegerEnv = (name, fallback) => {
|
|
|
5541
5561
|
const parsedValue = Number(rawValue);
|
|
5542
5562
|
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : fallback;
|
|
5543
5563
|
};
|
|
5544
|
-
var DEFAULT_KLINE_CONCURRENCY_LIMIT = NODE_ENV === "production" ?
|
|
5564
|
+
var DEFAULT_KLINE_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 5 : 10;
|
|
5545
5565
|
var KLINE_CONCURRENCY_LIMIT = getPositiveIntegerEnv(
|
|
5546
5566
|
"KLINE_CONCURRENCY_LIMIT",
|
|
5547
5567
|
DEFAULT_KLINE_CONCURRENCY_LIMIT
|
|
@@ -5551,10 +5571,11 @@ var SCREENSHOT_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 1 : 2;
|
|
|
5551
5571
|
|
|
5552
5572
|
// src/cli.ts
|
|
5553
5573
|
var import_files = require("@tradejs/infra/files");
|
|
5554
|
-
var
|
|
5574
|
+
var import_redis3 = require("@tradejs/infra/redis");
|
|
5555
5575
|
var import_logger5 = require("@tradejs/infra/logger");
|
|
5556
5576
|
|
|
5557
5577
|
// src/ai.ts
|
|
5578
|
+
var import_aiLanguages = require("@tradejs/infra/aiLanguages");
|
|
5558
5579
|
var import_redis = require("@tradejs/infra/redis");
|
|
5559
5580
|
var import_userSettings = require("@tradejs/infra/userSettings");
|
|
5560
5581
|
|
|
@@ -5582,6 +5603,109 @@ var trimSeriesDeep = (value) => {
|
|
|
5582
5603
|
return value;
|
|
5583
5604
|
};
|
|
5584
5605
|
|
|
5606
|
+
// src/aiMarketContext.ts
|
|
5607
|
+
var SESSION_WINDOWS = [
|
|
5608
|
+
{ name: "asia", startMinuteUtc: 0, endMinuteUtc: 8 * 60 },
|
|
5609
|
+
{ name: "europe", startMinuteUtc: 7 * 60, endMinuteUtc: 16 * 60 },
|
|
5610
|
+
{ name: "us", startMinuteUtc: 13 * 60, endMinuteUtc: 22 * 60 }
|
|
5611
|
+
];
|
|
5612
|
+
var toRecord = (value) => {
|
|
5613
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5614
|
+
return null;
|
|
5615
|
+
}
|
|
5616
|
+
return value;
|
|
5617
|
+
};
|
|
5618
|
+
var toFiniteNumber = (value) => {
|
|
5619
|
+
const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
|
|
5620
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
5621
|
+
};
|
|
5622
|
+
var getLastFiniteNumber = (value) => {
|
|
5623
|
+
const numeric = toFiniteNumber(value);
|
|
5624
|
+
if (numeric != null) {
|
|
5625
|
+
return numeric;
|
|
5626
|
+
}
|
|
5627
|
+
if (!Array.isArray(value)) {
|
|
5628
|
+
return null;
|
|
5629
|
+
}
|
|
5630
|
+
for (let i = value.length - 1; i >= 0; i -= 1) {
|
|
5631
|
+
const nested = getLastFiniteNumber(value[i]);
|
|
5632
|
+
if (nested != null) {
|
|
5633
|
+
return nested;
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
return null;
|
|
5637
|
+
};
|
|
5638
|
+
var roundTo = (value, decimals) => {
|
|
5639
|
+
const factor = 10 ** decimals;
|
|
5640
|
+
return Math.round(value * factor) / factor;
|
|
5641
|
+
};
|
|
5642
|
+
var isInsideSession = (minuteUtc, startMinuteUtc, endMinuteUtc) => startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc;
|
|
5643
|
+
var buildTradingSessionContext = (timestamp) => {
|
|
5644
|
+
const date = new Date(timestamp);
|
|
5645
|
+
const utcHour = date.getUTCHours();
|
|
5646
|
+
const utcMinute = date.getUTCMinutes();
|
|
5647
|
+
const minuteUtc = utcHour * 60 + utcMinute;
|
|
5648
|
+
const activeSessions = SESSION_WINDOWS.filter(
|
|
5649
|
+
(session) => isInsideSession(minuteUtc, session.startMinuteUtc, session.endMinuteUtc)
|
|
5650
|
+
).map((session) => session.name);
|
|
5651
|
+
const primarySession = activeSessions.includes("us") ? "us" : activeSessions.includes("europe") ? "europe" : activeSessions.includes("asia") ? "asia" : "off_hours";
|
|
5652
|
+
return {
|
|
5653
|
+
timezone: "UTC",
|
|
5654
|
+
utcHour,
|
|
5655
|
+
utcMinute,
|
|
5656
|
+
primarySession,
|
|
5657
|
+
activeSessions,
|
|
5658
|
+
isOverlap: activeSessions.length > 1,
|
|
5659
|
+
overlap: activeSessions.length > 1 ? `${activeSessions.join("_")}_overlap` : null
|
|
5660
|
+
};
|
|
5661
|
+
};
|
|
5662
|
+
var buildMissingSpreadContext = () => ({
|
|
5663
|
+
source: "binance_coinbase_btc",
|
|
5664
|
+
indicatorKey: "payload.indicators.spread",
|
|
5665
|
+
available: false,
|
|
5666
|
+
value: null,
|
|
5667
|
+
bps: null,
|
|
5668
|
+
absBps: null,
|
|
5669
|
+
bias: null,
|
|
5670
|
+
severity: null
|
|
5671
|
+
});
|
|
5672
|
+
var buildSpreadContextFromValue = (spread) => {
|
|
5673
|
+
const value = roundTo(spread, 8);
|
|
5674
|
+
const bps = roundTo(value * 1e4, 2);
|
|
5675
|
+
const absBps = Math.abs(bps);
|
|
5676
|
+
const bias = Math.abs(bps) < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
|
|
5677
|
+
const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
|
|
5678
|
+
return {
|
|
5679
|
+
source: "binance_coinbase_btc",
|
|
5680
|
+
indicatorKey: "payload.indicators.spread",
|
|
5681
|
+
available: true,
|
|
5682
|
+
value,
|
|
5683
|
+
bps,
|
|
5684
|
+
absBps,
|
|
5685
|
+
bias,
|
|
5686
|
+
severity
|
|
5687
|
+
};
|
|
5688
|
+
};
|
|
5689
|
+
var readSpreadFromSignal = (signal) => {
|
|
5690
|
+
const indicatorSpread = getLastFiniteNumber(signal.indicators?.spread);
|
|
5691
|
+
if (indicatorSpread != null) {
|
|
5692
|
+
return indicatorSpread;
|
|
5693
|
+
}
|
|
5694
|
+
return getLastFiniteNumber(signal.additionalIndicators?.spread);
|
|
5695
|
+
};
|
|
5696
|
+
var buildAiMarketContext = (signal) => {
|
|
5697
|
+
const existingMarketContext = toRecord(
|
|
5698
|
+
signal.additionalIndicators?.marketContext
|
|
5699
|
+
);
|
|
5700
|
+
const existingSpread = toRecord(existingMarketContext?.binanceCoinbaseSpread);
|
|
5701
|
+
const spread = readSpreadFromSignal(signal);
|
|
5702
|
+
return {
|
|
5703
|
+
...existingMarketContext ?? {},
|
|
5704
|
+
tradingSession: buildTradingSessionContext(signal.timestamp),
|
|
5705
|
+
binanceCoinbaseSpread: spread != null ? buildSpreadContextFromValue(spread) : existingSpread ?? buildMissingSpreadContext()
|
|
5706
|
+
};
|
|
5707
|
+
};
|
|
5708
|
+
|
|
5585
5709
|
// src/strategy/manifests.ts
|
|
5586
5710
|
var import_indicators = require("@tradejs/core/indicators");
|
|
5587
5711
|
var import_logger2 = require("@tradejs/infra/logger");
|
|
@@ -5591,6 +5715,7 @@ var import_fs = __toESM(require("fs"));
|
|
|
5591
5715
|
var import_path = __toESM(require("path"));
|
|
5592
5716
|
var import_module = require("module");
|
|
5593
5717
|
var import_url = require("url");
|
|
5718
|
+
var import_config = require("@tradejs/core/config");
|
|
5594
5719
|
var import_logger = require("@tradejs/infra/logger");
|
|
5595
5720
|
var CONFIG_FILE_NAMES = [
|
|
5596
5721
|
"tradejs.config.ts",
|
|
@@ -5623,10 +5748,14 @@ var normalizeConfig = (rawConfig) => {
|
|
|
5623
5748
|
const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
|
|
5624
5749
|
const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
|
|
5625
5750
|
const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
|
|
5751
|
+
const hooks = (0, import_config.normalizeTradejsConfigHooks)(
|
|
5752
|
+
config.hooks
|
|
5753
|
+
);
|
|
5626
5754
|
return {
|
|
5627
5755
|
strategies: strategies2,
|
|
5628
5756
|
indicators,
|
|
5629
|
-
connectors
|
|
5757
|
+
connectors,
|
|
5758
|
+
...hooks ? { hooks } : {}
|
|
5630
5759
|
};
|
|
5631
5760
|
};
|
|
5632
5761
|
var getRequireFn = (cwd = getTradejsProjectCwd()) => (0, import_module.createRequire)(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
|
|
@@ -5996,24 +6125,36 @@ var strategies = new Proxy(
|
|
|
5996
6125
|
);
|
|
5997
6126
|
|
|
5998
6127
|
// src/strategyAdapters/ai.ts
|
|
5999
|
-
var
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6128
|
+
var toRecord2 = (value) => {
|
|
6129
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6130
|
+
return {};
|
|
6131
|
+
}
|
|
6132
|
+
return value;
|
|
6133
|
+
};
|
|
6134
|
+
var buildBaseAiPayload = (signal) => {
|
|
6135
|
+
const additionalIndicators = {
|
|
6136
|
+
...toRecord2(signal.additionalIndicators),
|
|
6137
|
+
marketContext: buildAiMarketContext(signal)
|
|
6138
|
+
};
|
|
6139
|
+
return {
|
|
6140
|
+
signal: {
|
|
6141
|
+
symbol: signal.symbol,
|
|
6142
|
+
signalId: signal.signalId,
|
|
6143
|
+
interval: signal.interval,
|
|
6144
|
+
direction: signal.direction,
|
|
6145
|
+
timestamp: signal.timestamp,
|
|
6146
|
+
strategy: signal.strategy,
|
|
6147
|
+
prices: {
|
|
6148
|
+
currentPrice: signal.prices.currentPrice,
|
|
6149
|
+
takeProfitPrice: signal.prices.takeProfitPrice,
|
|
6150
|
+
stopLossPrice: signal.prices.stopLossPrice
|
|
6151
|
+
}
|
|
6152
|
+
},
|
|
6153
|
+
figures: trimSeriesDeep(signal.figures ?? {}),
|
|
6154
|
+
indicators: trimSeriesDeep(signal.indicators),
|
|
6155
|
+
additionalIndicators: trimSeriesDeep(additionalIndicators)
|
|
6156
|
+
};
|
|
6157
|
+
};
|
|
6017
6158
|
var defaultAiAdapter = {};
|
|
6018
6159
|
var getStrategyAiAdapter = (strategy) => getStrategyManifest(strategy)?.aiAdapter ?? defaultAiAdapter;
|
|
6019
6160
|
var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy);
|
|
@@ -6041,8 +6182,8 @@ var parseAIResponse = (input) => {
|
|
|
6041
6182
|
if (!match) throw new Error("JSON block not found");
|
|
6042
6183
|
return JSON.parse(match[0]);
|
|
6043
6184
|
} catch (err) {
|
|
6044
|
-
console.error("
|
|
6045
|
-
console.log("
|
|
6185
|
+
console.error("Failed to parse AI response:", err);
|
|
6186
|
+
console.log("Raw AI response:", input);
|
|
6046
6187
|
return {};
|
|
6047
6188
|
}
|
|
6048
6189
|
};
|
|
@@ -6088,22 +6229,22 @@ var normalizeAnalysis = (raw) => {
|
|
|
6088
6229
|
};
|
|
6089
6230
|
};
|
|
6090
6231
|
var buildAiSystemPrompt = (signal) => `
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6232
|
+
You are an internal market-structure classifier for an already computed system signal.
|
|
6233
|
+
Analyze the provided JSON containing the trade, candles, indicators (for the coin and BTC across multiple timeframes), and strategy figures/context.
|
|
6234
|
+
Series data is already trimmed to the latest 5 values.
|
|
6094
6235
|
|
|
6095
|
-
|
|
6096
|
-
-
|
|
6097
|
-
-
|
|
6098
|
-
-
|
|
6099
|
-
-
|
|
6100
|
-
-
|
|
6101
|
-
-
|
|
6102
|
-
-
|
|
6103
|
-
-
|
|
6104
|
-
-
|
|
6236
|
+
Important:
|
|
6237
|
+
- Do not invent missing data.
|
|
6238
|
+
- This is an internal audit/classification task, not user-facing trading advice.
|
|
6239
|
+
- Do not generate execution instructions, do not replace the original thesis with a new one, and do not provide personalized investment advice.
|
|
6240
|
+
- Use the original signal direction and levels as the anchor, but you may state that the current structure does not support them.
|
|
6241
|
+
- Respect the source strategy specified in \`signal.strategy\`.
|
|
6242
|
+
- Your goal is to explain how well the observed structure matches the existing signal and how structurally confirmed it is right now.
|
|
6243
|
+
- Do not write vague statements like "there is momentum/slope" without tying them to the decision.
|
|
6244
|
+
- Write all user-visible text fields in the requested response language. If no explicit language instruction is provided later, default to English.
|
|
6245
|
+
- If confidence is incomplete, prefer cautious wording such as "likely", "not confirmed yet", or "probably" instead of categorical claims.
|
|
6105
6246
|
|
|
6106
|
-
|
|
6247
|
+
Return exactly one JSON object and nothing else:
|
|
6107
6248
|
|
|
6108
6249
|
{
|
|
6109
6250
|
"direction": payload.signal.direction | null,
|
|
@@ -6121,103 +6262,114 @@ var buildAiSystemPrompt = (signal) => `
|
|
|
6121
6262
|
"triggerInvalidation": string
|
|
6122
6263
|
}
|
|
6123
6264
|
|
|
6124
|
-
-
|
|
6125
|
-
-
|
|
6126
|
-
-
|
|
6127
|
-
-
|
|
6128
|
-
-
|
|
6129
|
-
-
|
|
6130
|
-
-
|
|
6131
|
-
-
|
|
6132
|
-
-
|
|
6133
|
-
-
|
|
6134
|
-
-
|
|
6135
|
-
-
|
|
6136
|
-
-
|
|
6137
|
-
-
|
|
6138
|
-
-
|
|
6139
|
-
-
|
|
6140
|
-
-
|
|
6265
|
+
- Do not add any other fields.
|
|
6266
|
+
- All numbers must be finite, with no \`NaN\` or \`Infinity\`.
|
|
6267
|
+
- All text fields must be short strings with no line breaks and no markdown lists.
|
|
6268
|
+
- \`direction\` is not a new trade idea. It is only a compatibility flag for the existing signal: either exactly \`payload.signal.direction\` or \`null\` if the current structure does not confirm that signal. Never propose the opposite direction.
|
|
6269
|
+
- \`quality\` is the structural confirmation level of the current signal right now, including timing and confirmations. It is not a general attractiveness score and not investment advice.
|
|
6270
|
+
- \`needRetest\` indicates whether an additional confirmation level is required before the current signal can be treated as structurally confirmed.
|
|
6271
|
+
- \`retestPrice\` is the key level that would confirm or invalidate the structure, or \`null\` if no extra level is needed or available.
|
|
6272
|
+
- \`takeProfitPrice\` and \`stopLossPrice\` must not be newly invented levels. If the levels already supplied in \`payload.signal.prices\` still look internally coherent relative to the current price and the confirmed signal, you may return them as an audit of existing levels; otherwise return \`null\`.
|
|
6273
|
+
- Use these fields as separate parts of the analysis:
|
|
6274
|
+
- \`setup\`: the current structural setup or trendline state.
|
|
6275
|
+
- \`confirmations\`: 2-4 concrete confirmations or conflicts from the coin indicators.
|
|
6276
|
+
- \`btcContext\`: whether BTC supports the idea, is neutral, or conflicts with it.
|
|
6277
|
+
- \`retestPlan\`: what must happen at the key level to confirm the structure, or why no extra level is needed.
|
|
6278
|
+
- \`riskLevels\`: a short note on whether the existing levels and risk structure are internally coherent, without creating a new trade plan.
|
|
6279
|
+
- \`qualityReason\`: why the quality score is what it is.
|
|
6280
|
+
- \`triggerInvalidation\`: what must happen to confirm the signal or what invalidates the current structural thesis.
|
|
6281
|
+
- \`comment\` is optional. If you include it, do not just duplicate the structured fields.
|
|
6141
6282
|
|
|
6142
|
-
|
|
6283
|
+
If the data is insufficient or the setup is weak, return \`"direction": null\`, \`quality <= 2\`, and explain why.
|
|
6143
6284
|
|
|
6144
|
-
|
|
6285
|
+
Input payload structure:
|
|
6145
6286
|
- payload.signal:
|
|
6146
6287
|
symbol, signalId, interval, direction, timestamp, strategy, prices
|
|
6147
6288
|
- payload.signal.prices:
|
|
6148
6289
|
currentPrice, takeProfitPrice, stopLossPrice
|
|
6149
6290
|
- payload.figures:
|
|
6150
|
-
|
|
6291
|
+
strategy-specific figures or geometry when available. Fields vary by strategy.
|
|
6151
6292
|
- payload.indicators:
|
|
6152
|
-
|
|
6293
|
+
indicator dictionaries and series for the coin and BTC; all series are already trimmed to the latest 5 values.
|
|
6153
6294
|
- payload.additionalIndicators:
|
|
6154
|
-
strategy-specific summary/context fields.
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
\
|
|
6158
|
-
\u2022
|
|
6159
|
-
|
|
6160
|
-
|
|
6295
|
+
strategy-specific summary/context fields. This is not noise; it contains derived fields deliberately passed by the strategy to help the decision.
|
|
6296
|
+
Examples: helperFlags, structureContext, spread, correlation, volatilitySummary.
|
|
6297
|
+
Always inspect \`payload.additionalIndicators.marketContext\` when present:
|
|
6298
|
+
\u2022 \`marketContext.tradingSession\`: UTC session at signal time: asia / europe / us / overlap / off_hours.
|
|
6299
|
+
\u2022 \`marketContext.binanceCoinbaseSpread\`: BTC spread between Coinbase and Binance from \`payload.indicators.spread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
|
|
6300
|
+
If those fields exist, use them as a more explicit hint instead of trying to re-derive the same idea from raw lines or points.
|
|
6301
|
+
If \`derivativesContext\` exists, it is a derived Coinalyze summary for the time of the signal. Coinalyze context is built only from \`BTCUSDT\` and \`ETHUSDT\` reference symbols, not for every target coin. \`targetSymbol\` is just the source signal coin. Use BTC/ETH open interest, funding, liquidations, and pressure/riskFlags as positioning context, not as an independent trade idea.
|
|
6302
|
+
Key patterns:
|
|
6303
|
+
\u2022 coin: \`maFast\`, \`atrPct\`, \`macd...\`, \`candles15m/candles1h/candles4h/candles1d\`, and \`*1h/*4h/*1d\`
|
|
6304
|
+
\u2022 BTC: \`btcMaFast\`, \`btcAtr\`, \`btcMacd...\`, \`btcCandles*\`, and \`btc*1h/*4h/*1d\`
|
|
6305
|
+
\u2022 strategy service keys are possible as well, for example \`correlation\`, \`spread\`, \`touches\`, \`distance\`
|
|
6161
6306
|
|
|
6162
|
-
|
|
6163
|
-
1
|
|
6164
|
-
2
|
|
6165
|
-
3
|
|
6166
|
-
4
|
|
6167
|
-
5
|
|
6168
|
-
6
|
|
6307
|
+
How to analyze, in order:
|
|
6308
|
+
1. Start with price structure and the setup geometry or context in \`payload.figures\`. This has higher priority than indicators.
|
|
6309
|
+
2. Then use \`payload.additionalIndicators\` when it contains explicit strategy-specific context such as line state, spread, correlation, and similar fields.
|
|
6310
|
+
3. Then assess confirmation or conflict from the current coin indicators.
|
|
6311
|
+
4. Then evaluate BTC context.
|
|
6312
|
+
5. Only after that choose \`direction\`, \`quality\`, and whether an extra confirmation level is required.
|
|
6313
|
+
6. If strong conflicts exist, reduce quality or set direction to \`null\`.
|
|
6169
6314
|
|
|
6170
|
-
|
|
6171
|
-
-
|
|
6172
|
-
-
|
|
6173
|
-
-
|
|
6174
|
-
-
|
|
6175
|
-
|
|
6315
|
+
Explicit conflict rules:
|
|
6316
|
+
- If the figure or price structure is invalid or doubtful, indicators must not rescue the setup.
|
|
6317
|
+
- If strategy-specific helper fields explicitly say the signal is not confirmed yet, lacks margin, or requires waiting, do not overstate quality.
|
|
6318
|
+
- If the structure is acceptable but BTC or key indicators noticeably conflict, quality is usually \`<= 3\`.
|
|
6319
|
+
- If \`derivativesContext.referenceContexts\` exists, check \`primaryReferenceSymbol\` first, then compare \`BTCUSDT\` and \`ETHUSDT\` as broad-market derivatives context. Do not search for Coinalyze data for \`targetSymbol\` unless \`targetSymbol\` itself is \`BTCUSDT\` or \`ETHUSDT\`.
|
|
6320
|
+
- If \`derivativesContext.summary.riskFlags\` contains \`crowded_long\` for a LONG or \`crowded_short\` for a SHORT, treat that as crowded positioning and do not overstate quality without strong structural confirmation.
|
|
6321
|
+
- If \`derivativesContext.summary.directionAligned=false\`, explicitly mention the derivatives conflict in \`confirmations\` or \`qualityReason\`.
|
|
6322
|
+
- If \`derivativesContext\` is absent, stale, or \`missing_derivatives\`, do not infer Coinalyze conclusions and do not penalize the signal just because that data is missing.
|
|
6323
|
+
- If \`marketContext.tradingSession\` exists, treat the session as a liquidity and volatility regime: asia is often thinner, europe/us are more active, and overlaps can amplify both momentum and noise. Do not reject a signal solely because of session, but mention clear session support or conflict in \`confirmations\` or \`qualityReason\`.
|
|
6324
|
+
- If \`marketContext.binanceCoinbaseSpread.available=true\` and \`severity=elevated/wide\`, treat it as cross-exchange divergence or BTC liquidity risk. Do not use the spread as a standalone long/short signal, but reduce confidence or require more confirmation when the rest of the structure is weak or BTC context conflicts.
|
|
6325
|
+
- If \`marketContext.binanceCoinbaseSpread\` is missing or \`available=false\`, do not infer anything from Binance/Coinbase spread and do not penalize the signal just because it is absent.
|
|
6326
|
+
- If the current signal is not confirmed (\`direction=null\`), name the main reason briefly in \`comment\`.
|
|
6327
|
+
If you use the structured fields, include the main reason in \`qualityReason\` or \`triggerInvalidation\`.
|
|
6176
6328
|
|
|
6177
|
-
|
|
6178
|
-
- direction = LONG
|
|
6179
|
-
-
|
|
6180
|
-
-
|
|
6181
|
-
-
|
|
6182
|
-
-
|
|
6183
|
-
-
|
|
6184
|
-
-
|
|
6185
|
-
-
|
|
6329
|
+
Rules for \`direction\` / TP / SL:
|
|
6330
|
+
- \`direction = LONG\` only if the data confirms the existing LONG signal; \`SHORT\` only if the data confirms the existing SHORT signal; otherwise \`null\`.
|
|
6331
|
+
- For LONG, the expected relation is usually \`stopLossPrice < currentPrice < takeProfitPrice\`.
|
|
6332
|
+
- For SHORT, the expected relation is usually \`takeProfitPrice < currentPrice < stopLossPrice\`.
|
|
6333
|
+
- Do not optimize or recalculate TP/SL for a "better trade"; only assess whether the already supplied levels are coherent.
|
|
6334
|
+
- If \`direction = null\`, then \`takeProfitPrice = null\` and \`stopLossPrice = null\`.
|
|
6335
|
+
- If \`needRetest = false\`, then \`retestPrice = null\`.
|
|
6336
|
+
- If \`needRetest = true\`, \`retestPrice\` must be a finite number tied to a meaningful retest or breakout level.
|
|
6337
|
+
- Before responding, sanity-check the consistency of \`direction\`, TP/SL, and the current price.
|
|
6186
6338
|
|
|
6187
|
-
|
|
6188
|
-
- 1:
|
|
6189
|
-
- 2:
|
|
6190
|
-
- 3:
|
|
6191
|
-
- 4:
|
|
6192
|
-
- 5:
|
|
6339
|
+
Quality scale:
|
|
6340
|
+
- 1: poor or chaotic setup, strong conflicts, signal not structurally confirmed
|
|
6341
|
+
- 2: weak setup, few confirmations, more of a watch or reject
|
|
6342
|
+
- 3: average setup, some structure exists, but notable conflicts remain
|
|
6343
|
+
- 4: good setup, several confirmations, structure is mostly coherent
|
|
6344
|
+
- 5: very strong setup, clean structure, confirmations, and internally coherent levels
|
|
6193
6345
|
|
|
6194
|
-
|
|
6195
|
-
-
|
|
6196
|
-
-
|
|
6197
|
-
-
|
|
6198
|
-
-
|
|
6199
|
-
-
|
|
6200
|
-
-
|
|
6201
|
-
-
|
|
6346
|
+
Requirements for useful structured analysis:
|
|
6347
|
+
- Include 2-4 concrete factors for or against confirmation in \`confirmations\`.
|
|
6348
|
+
- Explicitly mention the role of the key figure or structural state, for example breakout, retest, false break, touch, or lack of confirmation.
|
|
6349
|
+
- Explicitly mention BTC context as supportive, neutral, or conflicting.
|
|
6350
|
+
- Explain why the quality score is what it is.
|
|
6351
|
+
- If the signal is not confirmed (\`direction=null\`), state clearly what must change for confirmation.
|
|
6352
|
+
- In \`retestPlan\`, avoid technical placeholders like \`needRetest=false @ null\`; write a human explanation.
|
|
6353
|
+
- Do not simply restate JSON fields; add interpretation and decision logic.
|
|
6202
6354
|
|
|
6203
|
-
|
|
6204
|
-
-
|
|
6205
|
-
-
|
|
6206
|
-
-
|
|
6355
|
+
Rules for using trimmed series (last 5 values):
|
|
6356
|
+
- Do not make strong long-term conclusions from only 5 points.
|
|
6357
|
+
- Use 4h and 1d series as brief context, not full history.
|
|
6358
|
+
- If the data is too limited for confidence, reduce quality and use cautious wording.
|
|
6207
6359
|
|
|
6208
|
-
|
|
6209
|
-
{"direction":"LONG","quality":4,"needRetest":true,"retestPrice":100.2,"takeProfitPrice":101.5,"stopLossPrice":98.9,"setup":"
|
|
6210
|
-
{"direction":null,"quality":2,"needRetest":false,"retestPrice":null,"takeProfitPrice":null,"stopLossPrice":null,"setup":"
|
|
6360
|
+
Short few-shot examples:
|
|
6361
|
+
{"direction":"LONG","quality":4,"needRetest":true,"retestPrice":100.2,"takeProfitPrice":101.5,"stopLossPrice":98.9,"setup":"Likely trendline breakout upward, but the signal still needs a level check for confirmation.","confirmations":"The coin shows momentum support without obvious overheating, but confirmation is not fully clean yet.","btcContext":"BTC is neutral-to-supportive and does not conflict with the current LONG signal.","retestPlan":"The key level is 100.2; holding above it would confirm the signal structure.","riskLevels":"The supplied TP and SL remain on the correct sides of the current price and still look internally coherent.","qualityReason":"Quality=4 because the structure is solid, but an extra level confirmation is still preferable.","triggerInvalidation":"The structure confirms on a hold above the level and weakens on a move back under the line."}
|
|
6362
|
+
{"direction":null,"quality":2,"needRetest":false,"retestPrice":null,"takeProfitPrice":null,"stopLossPrice":null,"setup":"Touch or noise around the trendline without a convincing breakout.","confirmations":"Indicators are mixed and do not provide strong structural support.","btcContext":"BTC is either conflicting or not supportive of the current thesis.","retestPlan":"It is too early to define an extra level because a quality breakout is not present yet.","riskLevels":"The supplied levels should not be treated as confirmed while the structure remains weak.","qualityReason":"Quality=2 because timing is weak and confirmations are limited.","triggerInvalidation":"Wait for a clear breakout and confirmation from both the coin and BTC."}
|
|
6211
6363
|
|
|
6212
|
-
|
|
6364
|
+
Return only the JSON object, with no extra characters.
|
|
6213
6365
|
${signal ? buildAiSystemPromptAddonByStrategy(signal) : ""}
|
|
6214
6366
|
`;
|
|
6215
6367
|
var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
|
|
6216
6368
|
var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
|
|
6217
|
-
|
|
6218
|
-
|
|
6369
|
+
Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
|
|
6370
|
+
This is a structure-classification and audit task, not execution advice. Determine whether the current structure confirms the existing signal, how structurally coherent it is right now, whether an extra confirmation level is needed, and whether the already supplied levels in \`payload.signal.prices\` still look internally coherent. Do not replace the original thesis with a new one and do not invent new levels; return only the requested JSON.
|
|
6219
6371
|
|
|
6220
|
-
|
|
6372
|
+
Trade payload:
|
|
6221
6373
|
${JSON.stringify(payload)}
|
|
6222
6374
|
${buildAiHumanPromptAddonByStrategy(signal, payload)}
|
|
6223
6375
|
`;
|
|
@@ -6225,6 +6377,14 @@ var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
|
|
|
6225
6377
|
var userSettingsCache = /* @__PURE__ */ new Map();
|
|
6226
6378
|
var aiModelCache = /* @__PURE__ */ new Map();
|
|
6227
6379
|
var getAiModelCacheKey = (userName, modelName) => `${userName}::${modelName}`;
|
|
6380
|
+
var resolveAiModelName = (settings, requestedModelName) => {
|
|
6381
|
+
const explicitModelName = typeof requestedModelName === "string" ? requestedModelName.trim() : "";
|
|
6382
|
+
if (explicitModelName) {
|
|
6383
|
+
return explicitModelName;
|
|
6384
|
+
}
|
|
6385
|
+
const settingsModelName = typeof settings.AI_MODEL === "string" ? settings.AI_MODEL.trim() : "";
|
|
6386
|
+
return settingsModelName || DEFAULT_AI_MODEL;
|
|
6387
|
+
};
|
|
6228
6388
|
var getOpenRouterModelKwargs = (apiEndpoint) => {
|
|
6229
6389
|
const endpoint = String(apiEndpoint ?? "").trim();
|
|
6230
6390
|
if (!endpoint) {
|
|
@@ -6255,30 +6415,27 @@ var getAiSettings = async (userName = "root") => {
|
|
|
6255
6415
|
userSettingsCache.set(userName, settingsPromise);
|
|
6256
6416
|
}
|
|
6257
6417
|
const settings = await settingsPromise;
|
|
6258
|
-
if (!settings.
|
|
6418
|
+
if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
|
|
6259
6419
|
throw new Error(`AI settings are incomplete for user ${userName}`);
|
|
6260
6420
|
}
|
|
6261
6421
|
return settings;
|
|
6262
6422
|
};
|
|
6263
|
-
var createAiModel = async (userName = "root",
|
|
6423
|
+
var createAiModel = async (userName = "root", requestedModelName) => {
|
|
6424
|
+
const settings = await getAiSettings(userName);
|
|
6425
|
+
const modelName = resolveAiModelName(settings, requestedModelName);
|
|
6264
6426
|
const cacheKey = getAiModelCacheKey(userName, modelName);
|
|
6265
6427
|
let modelPromise = aiModelCache.get(cacheKey);
|
|
6266
6428
|
if (!modelPromise) {
|
|
6267
6429
|
modelPromise = (async () => {
|
|
6268
|
-
const
|
|
6269
|
-
|
|
6270
|
-
getAiSettings(userName)
|
|
6271
|
-
]);
|
|
6272
|
-
const modelKwargs = getOpenRouterModelKwargs(
|
|
6273
|
-
settings.OPENAI_API_ENDPOINT
|
|
6274
|
-
);
|
|
6430
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
6431
|
+
const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
|
|
6275
6432
|
return new ChatOpenAI({
|
|
6276
6433
|
temperature: 0.2,
|
|
6277
6434
|
modelName,
|
|
6278
|
-
apiKey: settings.
|
|
6435
|
+
apiKey: settings.AI_API_KEY,
|
|
6279
6436
|
...Object.keys(modelKwargs).length ? { modelKwargs } : {},
|
|
6280
6437
|
configuration: {
|
|
6281
|
-
baseURL: settings.
|
|
6438
|
+
baseURL: settings.AI_API_ENDPOINT,
|
|
6282
6439
|
defaultHeaders: {
|
|
6283
6440
|
"HTTP-Referer": "https://tradejs.dev",
|
|
6284
6441
|
"X-Title": "Inv"
|
|
@@ -6293,11 +6450,13 @@ var createAiModel = async (userName = "root", modelName = DEFAULT_AI_MODEL) => {
|
|
|
6293
6450
|
}
|
|
6294
6451
|
return modelPromise;
|
|
6295
6452
|
};
|
|
6296
|
-
var getAiModel = async (userName = "root",
|
|
6453
|
+
var getAiModel = async (userName = "root", requestedModelName) => {
|
|
6454
|
+
const settings = await getAiSettings(userName);
|
|
6455
|
+
const resolvedModelName = resolveAiModelName(settings, requestedModelName);
|
|
6297
6456
|
try {
|
|
6298
|
-
return await createAiModel(userName,
|
|
6457
|
+
return await createAiModel(userName, resolvedModelName);
|
|
6299
6458
|
} catch (error) {
|
|
6300
|
-
aiModelCache.delete(getAiModelCacheKey(userName,
|
|
6459
|
+
aiModelCache.delete(getAiModelCacheKey(userName, resolvedModelName));
|
|
6301
6460
|
userSettingsCache.delete(userName);
|
|
6302
6461
|
throw error;
|
|
6303
6462
|
}
|
|
@@ -6309,12 +6468,21 @@ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
|
|
|
6309
6468
|
if (options.signal) {
|
|
6310
6469
|
await ensureAiStrategyPluginsLoaded();
|
|
6311
6470
|
}
|
|
6312
|
-
const [{ HumanMessage, SystemMessage }, model] = await Promise.all([
|
|
6471
|
+
const [{ HumanMessage, SystemMessage }, model, settings] = await Promise.all([
|
|
6313
6472
|
import("@langchain/core/messages"),
|
|
6314
|
-
getAiModel(options.userName, options.model)
|
|
6473
|
+
getAiModel(options.userName, options.model),
|
|
6474
|
+
getAiSettings(options.userName)
|
|
6315
6475
|
]);
|
|
6316
6476
|
const messages = [];
|
|
6477
|
+
const responseLanguage = (0, import_aiLanguages.getAiResponseLanguagePromptName)(
|
|
6478
|
+
settings.AI_RESPONSE_LANGUAGE || import_aiLanguages.DEFAULT_AI_RESPONSE_LANGUAGE
|
|
6479
|
+
);
|
|
6317
6480
|
messages.push(new SystemMessage(systemPrompt));
|
|
6481
|
+
messages.push(
|
|
6482
|
+
new SystemMessage(
|
|
6483
|
+
`Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
|
|
6484
|
+
)
|
|
6485
|
+
);
|
|
6318
6486
|
messages.push(
|
|
6319
6487
|
new HumanMessage({
|
|
6320
6488
|
content: [
|
|
@@ -6364,7 +6532,7 @@ var import_path2 = __toESM(require("path"));
|
|
|
6364
6532
|
var import_puppeteer = __toESM(require("puppeteer"));
|
|
6365
6533
|
var import_async = require("@tradejs/core/async");
|
|
6366
6534
|
var import_logger3 = require("@tradejs/infra/logger");
|
|
6367
|
-
var
|
|
6535
|
+
var import_redis2 = require("@tradejs/infra/redis");
|
|
6368
6536
|
var { APP_URL } = process.env;
|
|
6369
6537
|
var SCREENSHOT_NAVIGATION_ATTEMPTS = 3;
|
|
6370
6538
|
var SCREENSHOT_NAVIGATION_RETRY_DELAY_MS = 2e3;
|
|
@@ -6383,7 +6551,7 @@ var SCREENSHOT_VIEWPORT = {
|
|
|
6383
6551
|
};
|
|
6384
6552
|
var getProjectRoot = (projectRoot) => import_path2.default.resolve(getTradejsProjectCwd(projectRoot));
|
|
6385
6553
|
var getScreenshotsDir = (projectRoot) => import_path2.default.join(getProjectRoot(projectRoot), "data", "screenshots");
|
|
6386
|
-
var maskTokenInUrl = (url) => url.replace(/([?&]
|
|
6554
|
+
var maskTokenInUrl = (url) => url.replace(/([?&]screenshotToken=)[^&]+/i, "$1<hidden>");
|
|
6387
6555
|
var getErrorFields = (value) => {
|
|
6388
6556
|
if (!value || typeof value !== "object") {
|
|
6389
6557
|
return [];
|
|
@@ -6467,10 +6635,13 @@ var screenDashboard = async (signal, projectRoot, userName = "root") => {
|
|
|
6467
6635
|
const { symbol, signalId, interval } = signal;
|
|
6468
6636
|
const screenshotBaseUrl = getScreenshotRenderBaseUrl();
|
|
6469
6637
|
const screenshotPath = getScreenshotPath(signal, projectRoot);
|
|
6470
|
-
const
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6638
|
+
const screenshotToken = await (0, import_redis2.createScreenshotSessionToken)(userName);
|
|
6639
|
+
if (!screenshotToken) {
|
|
6640
|
+
throw new Error(
|
|
6641
|
+
`Failed to create screenshot session token for ${userName}`
|
|
6642
|
+
);
|
|
6643
|
+
}
|
|
6644
|
+
const dashboardUrl = `${screenshotBaseUrl}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true&screenshot=1&screenshotToken=${encodeURIComponent(screenshotToken)}`;
|
|
6474
6645
|
const maskedDashboardUrl = maskTokenInUrl(dashboardUrl);
|
|
6475
6646
|
import_logger3.logger.info(
|
|
6476
6647
|
"screenshot start: %s %sm url=%s path=%s",
|
|
@@ -6726,7 +6897,7 @@ var screenDashboard = async (signal, projectRoot, userName = "root") => {
|
|
|
6726
6897
|
var import_async2 = require("@tradejs/core/async");
|
|
6727
6898
|
var import_math = require("@tradejs/core/math");
|
|
6728
6899
|
var import_logger4 = require("@tradejs/infra/logger");
|
|
6729
|
-
var
|
|
6900
|
+
var import_userSettings2 = require("@tradejs/infra/userSettings");
|
|
6730
6901
|
var escapeHtml = (s) => {
|
|
6731
6902
|
if (!s) return "";
|
|
6732
6903
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -6778,11 +6949,51 @@ var describeErrorValue2 = (value) => {
|
|
|
6778
6949
|
return String(value);
|
|
6779
6950
|
};
|
|
6780
6951
|
var normalizeQuality = (value) => typeof value === "number" ? Math.max(1, Math.min(5, Math.round(value))) : null;
|
|
6952
|
+
var formatAnalysisLevel = (value) => {
|
|
6953
|
+
if (value == null || !Number.isFinite(value)) return null;
|
|
6954
|
+
if (Math.abs(value) >= 1e3) {
|
|
6955
|
+
return value.toFixed(0);
|
|
6956
|
+
}
|
|
6957
|
+
const digits = Math.abs(value) < 1 ? 8 : 6;
|
|
6958
|
+
return value.toFixed(digits).replace(/\.?0+$/, "");
|
|
6959
|
+
};
|
|
6960
|
+
var normalizeAnalysisText = (value) => {
|
|
6961
|
+
if (typeof value !== "string") return null;
|
|
6962
|
+
const clean = value.replace(/\s+/g, " ").trim();
|
|
6963
|
+
return clean || null;
|
|
6964
|
+
};
|
|
6965
|
+
var getReadableAnalysisComment = (value) => {
|
|
6966
|
+
const clean = normalizeAnalysisText(value);
|
|
6967
|
+
if (!clean) return null;
|
|
6968
|
+
if (/^(ok|approved|approved by ai|historical approval|historical reject)$/i.test(
|
|
6969
|
+
clean
|
|
6970
|
+
)) {
|
|
6971
|
+
return null;
|
|
6972
|
+
}
|
|
6973
|
+
return clean;
|
|
6974
|
+
};
|
|
6975
|
+
var takeUniqueAnalysisText = (usedValues, ...values) => {
|
|
6976
|
+
for (const value of values) {
|
|
6977
|
+
const clean = normalizeAnalysisText(value);
|
|
6978
|
+
if (!clean) continue;
|
|
6979
|
+
const key = clean.toLowerCase();
|
|
6980
|
+
if (usedValues.has(key)) continue;
|
|
6981
|
+
usedValues.add(key);
|
|
6982
|
+
return clean;
|
|
6983
|
+
}
|
|
6984
|
+
return null;
|
|
6985
|
+
};
|
|
6781
6986
|
var formatOrderSkipReason = (reason) => {
|
|
6782
6987
|
if (!reason) return "";
|
|
6783
6988
|
if (reason.startsWith("AI_QUALITY_BELOW_MIN")) {
|
|
6784
6989
|
return "AI_QUALITY_BELOW_MIN";
|
|
6785
6990
|
}
|
|
6991
|
+
if (reason.startsWith("ML_THRESHOLD_NOT_MET")) {
|
|
6992
|
+
return "ML_THRESHOLD_NOT_MET";
|
|
6993
|
+
}
|
|
6994
|
+
if (reason === "ML_RESULT_UNAVAILABLE") {
|
|
6995
|
+
return "ML_RESULT_UNAVAILABLE";
|
|
6996
|
+
}
|
|
6786
6997
|
return reason;
|
|
6787
6998
|
};
|
|
6788
6999
|
var getLastNumber = (value) => {
|
|
@@ -6855,7 +7066,7 @@ var parseTelegramResponse = async (response) => {
|
|
|
6855
7066
|
}
|
|
6856
7067
|
};
|
|
6857
7068
|
var getTelegramSettings = async (userName = "root") => {
|
|
6858
|
-
const settings = await (0,
|
|
7069
|
+
const settings = await (0, import_userSettings2.getUserSettings)(userName);
|
|
6859
7070
|
const token = settings.TG_BOT_TOKEN;
|
|
6860
7071
|
const chatId = settings.TG_CHAT_ID;
|
|
6861
7072
|
if (!token || !chatId) {
|
|
@@ -6919,6 +7130,15 @@ var sendTelegramMessage = async ({
|
|
|
6919
7130
|
);
|
|
6920
7131
|
return data;
|
|
6921
7132
|
};
|
|
7133
|
+
var sendTextToTG = async (message, options = {}) => {
|
|
7134
|
+
const { token, chatId } = await getTelegramSettings(options.userName);
|
|
7135
|
+
return sendTelegramMessage({
|
|
7136
|
+
message,
|
|
7137
|
+
markup: options.markup,
|
|
7138
|
+
token,
|
|
7139
|
+
chatId
|
|
7140
|
+
});
|
|
7141
|
+
};
|
|
6922
7142
|
var formatMessage = (signal, analysis) => {
|
|
6923
7143
|
const {
|
|
6924
7144
|
symbol,
|
|
@@ -7091,43 +7311,78 @@ Reason: <code>${escapeHtml(reason)}</code>`,
|
|
|
7091
7311
|
};
|
|
7092
7312
|
var formatAnalysisMessage = (signal, analysis) => {
|
|
7093
7313
|
const lines = [];
|
|
7094
|
-
const blocks = [];
|
|
7095
7314
|
const quality = normalizeQuality(analysis.quality);
|
|
7315
|
+
const readableComment = getReadableAnalysisComment(analysis.comment);
|
|
7316
|
+
const approvedDirection = analysis.direction != null && analysis.direction === signal.direction && analysis.needRetest !== true;
|
|
7317
|
+
const oppositeDirection = analysis.direction != null && analysis.direction !== signal.direction;
|
|
7318
|
+
const verdict = approvedDirection ? `Approved ${signal.direction}` : oppositeDirection ? `Not approved: AI prefers ${analysis.direction}` : analysis.needRetest ? "Not approved yet" : "Not approved";
|
|
7319
|
+
const summary = approvedDirection ? `AI approves this ${signal.direction} setup right now.` : oppositeDirection ? `AI does not approve this ${signal.direction} setup and currently leans ${analysis.direction}.` : analysis.needRetest ? `The ${signal.direction} setup is visible, but confirmation is still missing before entry.` : `AI does not approve this ${signal.direction} setup right now.`;
|
|
7320
|
+
const usedValues = /* @__PURE__ */ new Set();
|
|
7096
7321
|
lines.push(`<b>AI analysis ${signal.symbol}</b>`);
|
|
7097
|
-
lines.push(`
|
|
7098
|
-
lines.push(
|
|
7322
|
+
lines.push(`Verdict: <b>${escapeHtml(verdict)}</b>`);
|
|
7323
|
+
lines.push(escapeHtml(summary));
|
|
7099
7324
|
if (quality) {
|
|
7100
7325
|
lines.push(`Quality: <b>${quality}/5</b>`);
|
|
7101
7326
|
}
|
|
7102
|
-
if (
|
|
7103
|
-
lines.push(
|
|
7327
|
+
if (analysis.gateDecision && analysis.llmDecision) {
|
|
7328
|
+
lines.push(
|
|
7329
|
+
`Gate vs LLM: <b>${analysis.gateContradictsLlm ? "conflict" : "aligned"}</b> (gate ${analysis.gateDecision}, LLM ${analysis.llmDecision})`
|
|
7330
|
+
);
|
|
7104
7331
|
}
|
|
7105
|
-
|
|
7106
|
-
|
|
7332
|
+
const happeningText = takeUniqueAnalysisText(
|
|
7333
|
+
usedValues,
|
|
7334
|
+
analysis.setup,
|
|
7335
|
+
readableComment
|
|
7336
|
+
);
|
|
7337
|
+
if (happeningText) {
|
|
7338
|
+
lines.push(`What's happening: ${escapeHtml(happeningText)}`);
|
|
7339
|
+
}
|
|
7340
|
+
const reasonText = approvedDirection ? takeUniqueAnalysisText(
|
|
7341
|
+
usedValues,
|
|
7342
|
+
analysis.qualityReason,
|
|
7343
|
+
analysis.confirmations,
|
|
7344
|
+
readableComment
|
|
7345
|
+
) : takeUniqueAnalysisText(
|
|
7346
|
+
usedValues,
|
|
7347
|
+
analysis.qualityReason,
|
|
7348
|
+
analysis.confirmations,
|
|
7349
|
+
readableComment,
|
|
7350
|
+
analysis.btcContext
|
|
7351
|
+
);
|
|
7352
|
+
if (reasonText) {
|
|
7353
|
+
lines.push(
|
|
7354
|
+
`${approvedDirection ? "Why approved" : "Why not approved"}: ${escapeHtml(reasonText)}`
|
|
7355
|
+
);
|
|
7107
7356
|
}
|
|
7357
|
+
const nextText = analysis.needRetest ? takeUniqueAnalysisText(
|
|
7358
|
+
usedValues,
|
|
7359
|
+
analysis.retestPlan,
|
|
7360
|
+
analysis.triggerInvalidation,
|
|
7361
|
+
typeof analysis.retestPrice === "number" ? `Wait for confirmation around ${formatAnalysisLevel(analysis.retestPrice)}.` : null
|
|
7362
|
+
) : takeUniqueAnalysisText(
|
|
7363
|
+
usedValues,
|
|
7364
|
+
analysis.triggerInvalidation,
|
|
7365
|
+
analysis.retestPlan
|
|
7366
|
+
);
|
|
7367
|
+
if (nextText) {
|
|
7368
|
+
lines.push(`Next: ${escapeHtml(nextText)}`);
|
|
7369
|
+
}
|
|
7370
|
+
const btcText = takeUniqueAnalysisText(usedValues, analysis.btcContext);
|
|
7371
|
+
if (btcText) {
|
|
7372
|
+
lines.push(`BTC context: ${escapeHtml(btcText)}`);
|
|
7373
|
+
}
|
|
7374
|
+
const levels = [];
|
|
7108
7375
|
if (typeof analysis.takeProfitPrice === "number") {
|
|
7109
|
-
|
|
7376
|
+
levels.push(`TP <b>${formatAnalysisLevel(analysis.takeProfitPrice)}</b>`);
|
|
7110
7377
|
}
|
|
7111
7378
|
if (typeof analysis.stopLossPrice === "number") {
|
|
7112
|
-
|
|
7379
|
+
levels.push(`SL <b>${formatAnalysisLevel(analysis.stopLossPrice)}</b>`);
|
|
7113
7380
|
}
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
${escapeHtml(clean)}`);
|
|
7120
|
-
};
|
|
7121
|
-
pushBlock("Setup", analysis.setup);
|
|
7122
|
-
pushBlock("Confirmations", analysis.confirmations);
|
|
7123
|
-
pushBlock("BTC", analysis.btcContext);
|
|
7124
|
-
pushBlock("Retest", analysis.retestPlan);
|
|
7125
|
-
pushBlock("Risk/Levels", analysis.riskLevels);
|
|
7126
|
-
pushBlock("Why Quality", analysis.qualityReason);
|
|
7127
|
-
pushBlock("Trigger/Invalidation", analysis.triggerInvalidation);
|
|
7128
|
-
if (blocks.length > 0) {
|
|
7129
|
-
lines.push("");
|
|
7130
|
-
lines.push(blocks.join("\n\n"));
|
|
7381
|
+
if (typeof analysis.retestPrice === "number") {
|
|
7382
|
+
levels.push(`Retest <b>${formatAnalysisLevel(analysis.retestPrice)}</b>`);
|
|
7383
|
+
}
|
|
7384
|
+
if (levels.length > 0) {
|
|
7385
|
+
lines.push(`Levels: ${levels.join(" | ")}`);
|
|
7131
7386
|
}
|
|
7132
7387
|
return lines.join("\n");
|
|
7133
7388
|
};
|
|
@@ -7181,7 +7436,7 @@ var cleanFiles = async (dir) => {
|
|
|
7181
7436
|
};
|
|
7182
7437
|
var cleanRedis = async (area) => {
|
|
7183
7438
|
let completed = 0;
|
|
7184
|
-
const keys = await (0,
|
|
7439
|
+
const keys = await (0, import_redis3.getKeys)(area);
|
|
7185
7440
|
const bar = new import_progress.default(":current/:total [:bar][:percent] :eta(s)", {
|
|
7186
7441
|
total: keys.length,
|
|
7187
7442
|
width: 30
|
|
@@ -7190,13 +7445,13 @@ var cleanRedis = async (area) => {
|
|
|
7190
7445
|
try {
|
|
7191
7446
|
for await (const key of keys) {
|
|
7192
7447
|
completed++;
|
|
7193
|
-
await (0,
|
|
7448
|
+
await (0, import_redis3.delKeyWithOptions)(key, { raiseOnMisconf: true });
|
|
7194
7449
|
if (completed % 100 === 0 || completed === keys.length) {
|
|
7195
7450
|
bar.tick(completed === keys.length ? completed % 100 : 100);
|
|
7196
7451
|
}
|
|
7197
7452
|
}
|
|
7198
7453
|
} catch (e) {
|
|
7199
|
-
if (e instanceof
|
|
7454
|
+
if (e instanceof import_redis3.RedisWriteBlockedError || String(e).includes("MISCONF")) {
|
|
7200
7455
|
import_logger5.logger.error(
|
|
7201
7456
|
"Redis write is blocked by MISCONF (RDB save failure). Cleanup stopped early."
|
|
7202
7457
|
);
|
|
@@ -7210,9 +7465,18 @@ var cleanRedis = async (area) => {
|
|
|
7210
7465
|
}
|
|
7211
7466
|
import_logger5.logger.info("");
|
|
7212
7467
|
};
|
|
7213
|
-
var update = async (connector, interval, tickers, preloadDays = import_constants.PRELOAD_DAYS) => {
|
|
7214
|
-
const
|
|
7215
|
-
|
|
7468
|
+
var update = async (connector, interval, tickers, preloadDays = import_constants.PRELOAD_DAYS, options = {}) => {
|
|
7469
|
+
const preloadStart = Math.trunc(
|
|
7470
|
+
options.preloadStart ?? (0, import_time.getTimestamp)(preloadDays)
|
|
7471
|
+
);
|
|
7472
|
+
const preloadEnd = Math.trunc(options.preloadEnd ?? (0, import_time.getTimestamp)());
|
|
7473
|
+
const connectorLabel = String(options.connectorLabel || "").trim();
|
|
7474
|
+
const preloadLabel = options.preloadStart != null || options.preloadEnd != null ? `preloadStart=${preloadStart}, preloadEnd=${preloadEnd}` : `preloadDays=${preloadDays}`;
|
|
7475
|
+
if (preloadStart >= preloadEnd) {
|
|
7476
|
+
throw new Error(
|
|
7477
|
+
`Invalid update preload window: start (${preloadStart}) must be less than end (${preloadEnd})`
|
|
7478
|
+
);
|
|
7479
|
+
}
|
|
7216
7480
|
const bar = new import_progress.default(
|
|
7217
7481
|
":current/:total [:bar][:percent] :eta(s) :symbol",
|
|
7218
7482
|
{
|
|
@@ -7222,7 +7486,7 @@ var update = async (connector, interval, tickers, preloadDays = import_constants
|
|
|
7222
7486
|
);
|
|
7223
7487
|
import_logger5.logger.info(
|
|
7224
7488
|
import_chalk.default.yellow(
|
|
7225
|
-
`update: ${tickers.length} (klineConcurrency=${KLINE_CONCURRENCY_LIMIT},
|
|
7489
|
+
`update: ${tickers.length} (connector=${connectorLabel || "unknown"}, klineConcurrency=${KLINE_CONCURRENCY_LIMIT}, ${preloadLabel})`
|
|
7226
7490
|
)
|
|
7227
7491
|
);
|
|
7228
7492
|
const queue = tickers.slice();
|
|
@@ -7233,8 +7497,8 @@ var update = async (connector, interval, tickers, preloadDays = import_constants
|
|
|
7233
7497
|
try {
|
|
7234
7498
|
await connector.kline({
|
|
7235
7499
|
symbol,
|
|
7236
|
-
start:
|
|
7237
|
-
end:
|
|
7500
|
+
start: preloadStart,
|
|
7501
|
+
end: preloadEnd,
|
|
7238
7502
|
interval,
|
|
7239
7503
|
silent: true,
|
|
7240
7504
|
warmOnly: true
|
|
@@ -7340,18 +7604,25 @@ var sendToAI = async (signals, userName = "root") => {
|
|
|
7340
7604
|
import_logger5.logger.info("");
|
|
7341
7605
|
};
|
|
7342
7606
|
var sendToTG = async (signals, imgInterval, userName = "root") => {
|
|
7607
|
+
const deliverableSignals = signals.filter(
|
|
7608
|
+
(signal) => signal.orderStatus !== "skipped" && signal.orderStatus !== "canceled"
|
|
7609
|
+
);
|
|
7610
|
+
import_logger5.logger.info(import_chalk.default.yellow("messages:", deliverableSignals.length));
|
|
7611
|
+
if (!deliverableSignals.length) {
|
|
7612
|
+
import_logger5.logger.info("");
|
|
7613
|
+
return;
|
|
7614
|
+
}
|
|
7343
7615
|
const bar = new import_progress.default(
|
|
7344
7616
|
":current/:total [:bar][:percent] :eta(s) :symbol",
|
|
7345
7617
|
{
|
|
7346
|
-
total:
|
|
7618
|
+
total: deliverableSignals.length,
|
|
7347
7619
|
width: 30
|
|
7348
7620
|
}
|
|
7349
7621
|
);
|
|
7350
|
-
|
|
7351
|
-
await (0, import_async3.runWithConcurrency)(signals, 1, async (signal) => {
|
|
7622
|
+
await (0, import_async3.runWithConcurrency)(deliverableSignals, 1, async (signal) => {
|
|
7352
7623
|
try {
|
|
7353
|
-
const analysis = await (0,
|
|
7354
|
-
|
|
7624
|
+
const analysis = await (0, import_redis3.getData)(
|
|
7625
|
+
import_redis3.redisKeys.analysis(signal.symbol, signal.signalId),
|
|
7355
7626
|
null
|
|
7356
7627
|
);
|
|
7357
7628
|
await sendSignal(signal, imgInterval, analysis, { userName });
|
|
@@ -7376,7 +7647,9 @@ var sendToTG = async (signals, imgInterval, userName = "root") => {
|
|
|
7376
7647
|
cleanRedis,
|
|
7377
7648
|
drawStatInCLI,
|
|
7378
7649
|
getTickers,
|
|
7650
|
+
loadTradejsConfig,
|
|
7379
7651
|
makeScreenshots,
|
|
7652
|
+
sendTextToTG,
|
|
7380
7653
|
sendToAI,
|
|
7381
7654
|
sendToTG,
|
|
7382
7655
|
update
|