@tradejs/strategies 1.0.5 → 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.
@@ -0,0 +1,836 @@
1
+ // src/VolumeDivergence/adapters/ai.ts
2
+ import { mapAiRuntimeFromConfig } from "@tradejs/core/strategies";
3
+
4
+ // src/VolumeDivergence/setup.ts
5
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
6
+ var DEFAULT_VOLUME_DIVERGENCE_ENTRY_THRESHOLDS = {
7
+ allowStructureAdvanceEntry: false,
8
+ minDivergenceAmplitudeAtrRatio: 0.35,
9
+ minReclaimPct: 105,
10
+ minConfirmationCandleQuality: 0.58
11
+ };
12
+ var VOLUME_DIVERGENCE_AI_THRESHOLDS = {
13
+ LONG: {
14
+ q4DivergenceAmplitudeAtrRatio: 0.45,
15
+ q4ReclaimPct: 115,
16
+ q4ConfirmationCandleQuality: 0.62,
17
+ q5DivergenceAmplitudeAtrRatio: 0.8,
18
+ q5ReclaimPct: 145,
19
+ q5ConfirmationCandleQuality: 0.8
20
+ },
21
+ SHORT: {
22
+ q4DivergenceAmplitudeAtrRatio: 0.6,
23
+ q4ReclaimPct: 125,
24
+ q4ConfirmationCandleQuality: 0.7,
25
+ q5DivergenceAmplitudeAtrRatio: 0.95,
26
+ q5ReclaimPct: 160,
27
+ q5ConfirmationCandleQuality: 0.82
28
+ }
29
+ };
30
+ var getVolumeDivergenceEntryThresholds = ({
31
+ ALLOW_STRUCTURE_ADVANCE_ENTRY,
32
+ MIN_DIVERGENCE_AMPLITUDE_ATR_RATIO,
33
+ MIN_RECLAIM_PCT,
34
+ MIN_CONFIRMATION_CANDLE_QUALITY
35
+ }) => ({
36
+ allowStructureAdvanceEntry: ALLOW_STRUCTURE_ADVANCE_ENTRY,
37
+ minDivergenceAmplitudeAtrRatio: MIN_DIVERGENCE_AMPLITUDE_ATR_RATIO,
38
+ minReclaimPct: MIN_RECLAIM_PCT,
39
+ minConfirmationCandleQuality: MIN_CONFIRMATION_CANDLE_QUALITY
40
+ });
41
+ var getVolumeDivergenceAiThresholds = (direction) => VOLUME_DIVERGENCE_AI_THRESHOLDS[direction];
42
+ var calculateAverageTrueRange = (candles, period) => {
43
+ if (!Array.isArray(candles) || candles.length < 2 || period <= 0) {
44
+ return null;
45
+ }
46
+ let total = 0;
47
+ let count = 0;
48
+ const startIndex = Math.max(1, candles.length - period);
49
+ for (let i = startIndex; i < candles.length; i += 1) {
50
+ const candle = candles[i];
51
+ const previousClose = Number(candles[i - 1]?.close);
52
+ const high = Number(candle?.high);
53
+ const low = Number(candle?.low);
54
+ if (!Number.isFinite(previousClose) || !Number.isFinite(high) || !Number.isFinite(low)) {
55
+ continue;
56
+ }
57
+ const trueRange = Math.max(
58
+ high - low,
59
+ Math.abs(high - previousClose),
60
+ Math.abs(low - previousClose)
61
+ );
62
+ total += trueRange;
63
+ count += 1;
64
+ }
65
+ return count > 0 ? total / count : null;
66
+ };
67
+ var getConfirmationCandleQuality = ({
68
+ candle,
69
+ direction
70
+ }) => {
71
+ const high = Number(candle.high);
72
+ const low = Number(candle.low);
73
+ const open = Number(candle.open);
74
+ const close = Number(candle.close);
75
+ if (!Number.isFinite(high) || !Number.isFinite(low) || !Number.isFinite(open) || !Number.isFinite(close)) {
76
+ return null;
77
+ }
78
+ const range = Math.max(high - low, 1e-9);
79
+ const bodyPct = clamp(Math.abs(close - open) / range, 0, 1);
80
+ const closeLocation = direction === "LONG" ? clamp((close - low) / range, 0, 1) : clamp((high - close) / range, 0, 1);
81
+ return closeLocation * 0.7 + bodyPct * 0.3;
82
+ };
83
+ var calculateAtrPct = ({
84
+ atrAbsolute,
85
+ currentPrice
86
+ }) => atrAbsolute != null && currentPrice > 0 ? atrAbsolute / currentPrice * 100 : null;
87
+ var calculateDivergenceAmplitudeAtrRatio = ({
88
+ direction,
89
+ atrAbsolute,
90
+ currentPivotLow,
91
+ previousPivotLow,
92
+ currentPivotHigh,
93
+ previousPivotHigh
94
+ }) => {
95
+ const divergenceAmplitude = direction === "LONG" ? previousPivotLow - currentPivotLow : currentPivotHigh - previousPivotHigh;
96
+ return atrAbsolute != null && atrAbsolute > 0 && divergenceAmplitude > 0 ? divergenceAmplitude / atrAbsolute : null;
97
+ };
98
+ var calculateReclaimPct = ({
99
+ direction,
100
+ currentPrice,
101
+ currentPivotLow,
102
+ currentPivotHigh
103
+ }) => {
104
+ const reclaimRange = currentPivotHigh - currentPivotLow;
105
+ const reclaimProgress = direction === "LONG" ? currentPrice - currentPivotLow : currentPivotHigh - currentPrice;
106
+ return reclaimRange > 0 ? reclaimProgress / reclaimRange * 100 : null;
107
+ };
108
+ var calculateConfirmationDistancePct = ({
109
+ direction,
110
+ currentPrice,
111
+ currentPivotLow,
112
+ currentPivotHigh
113
+ }) => {
114
+ const confirmationPrice = direction === "LONG" ? currentPivotHigh : currentPivotLow;
115
+ if (!(confirmationPrice > 0)) {
116
+ return null;
117
+ }
118
+ return direction === "LONG" ? (currentPrice - confirmationPrice) / confirmationPrice * 100 : (confirmationPrice - currentPrice) / confirmationPrice * 100;
119
+ };
120
+ var buildVolumeDivergenceSetupFeatures = ({
121
+ candles,
122
+ currentCandle,
123
+ direction,
124
+ currentPrice,
125
+ currentPivotLow,
126
+ previousPivotLow,
127
+ currentPivotHigh,
128
+ previousPivotHigh,
129
+ atrPeriod
130
+ }) => {
131
+ const atrAbsolute = calculateAverageTrueRange(candles, atrPeriod);
132
+ const atrPct = calculateAtrPct({ atrAbsolute, currentPrice });
133
+ const divergenceAmplitudeAtrRatio = calculateDivergenceAmplitudeAtrRatio({
134
+ direction,
135
+ atrAbsolute,
136
+ currentPivotLow,
137
+ previousPivotLow,
138
+ currentPivotHigh,
139
+ previousPivotHigh
140
+ });
141
+ const reclaimPct = calculateReclaimPct({
142
+ direction,
143
+ currentPrice,
144
+ currentPivotLow,
145
+ currentPivotHigh
146
+ });
147
+ const confirmationCandleQuality = getConfirmationCandleQuality({
148
+ candle: currentCandle,
149
+ direction
150
+ });
151
+ const confirmationDistancePct = calculateConfirmationDistancePct({
152
+ direction,
153
+ currentPrice,
154
+ currentPivotLow,
155
+ currentPivotHigh
156
+ });
157
+ return {
158
+ atrAbsolute,
159
+ atrPct,
160
+ divergenceAmplitudeAtrRatio,
161
+ reclaimPct,
162
+ confirmationCandleQuality,
163
+ confirmationDistancePct
164
+ };
165
+ };
166
+
167
+ // src/VolumeDivergence/adapters/ai.ts
168
+ var VOLUME_DIVERGENCE_CONTEXT_PROMPT = `
169
+ VolumeDivergence addon:
170
+ - This is a reversal setup built on price and normalized-volume divergence, not a breakout strategy.
171
+ - Bullish divergence means price makes a lower low while volume makes a higher low.
172
+ - Bearish divergence means price makes a higher high while volume makes a lower high.
173
+ - For a bullish signal, do not overstate quality if price still failed to bounce meaningfully away from the current pivot low or failed to reclaim enough structure.
174
+ - For a bearish signal, mirror that logic: do not overstate quality if price failed to move down meaningfully away from the current pivot high.
175
+ - If \`payload.additionalIndicators.volumeDivergenceContext.confirmationReady=false\`, this is usually not a fully confirmed reversal yet; quality is often \`<= 4\` and a retest or confirmation is often still needed.
176
+ - For live approval, treat \`confirmationReady\` as much more important than \`structureAdvanced\`; structure advance alone does not mean the reversal is entry-ready.
177
+ - For a reversal setup, do not automatically reward quality just because the coin or BTC MA bias already matches the signal direction.
178
+ - For LONG with \`entryTiming=structure_advance\`, avoid \`quality=5\`; that is an intermediate phase, not a fully confirmed reversal.
179
+ - Be stricter for SHORT than for LONG: a bearish reversal should require cleaner follow-through, and bias or delta conflict should reduce quality more aggressively.
180
+ - If \`deltaAtPivot\` conflicts with the reversal direction or the coin/BTC bias conflicts with the signal, do not overstate quality just because divergence exists.
181
+ - Use \`divergenceAmplitudeAtrRatio\`, \`reclaimPct\`, and \`confirmationCandleQuality\` as explicit setup features describing how meaningful the divergence is relative to ATR, how much structure price reclaimed, and how strong the confirmation candle was.
182
+ - \`confirmationDistancePct\` tells you how far price moved beyond the confirmation level; do not overstate quality when confirmation exists only marginally.
183
+ - \`additionalIndicators.deltaAtPivot\` is a proxy net-volume value on the pivot candle, not true lower-timeframe TradingView volume delta.
184
+ - If \`payload.additionalIndicators.derivativesContext\` exists, use Coinalyze-derived open interest, funding, and liquidations as positioning context: a liquidation flush can strengthen reversal odds, while crowded positioning against the trade or stale or missing data should not mechanically increase quality.
185
+ `;
186
+ var VOLUME_DIVERGENCE_PAYLOAD_PROMPT = `
187
+ - \`payload.additionalIndicators.volumeDivergenceContext\` contains a compact divergence-strength summary:
188
+ divergenceKind / confirmationPrice / confirmationReady / structureAdvanced / reboundFromPivotPct / confirmationDistancePct / priceDisplacementPct / divergenceAmplitudeAtrRatio / reclaimPct / confirmationCandleQuality / volumeDivergenceStrength / deltaAligned / coinBiasAligned / btcBiasAligned / deterministicQuality / approvalAllowedNow / structuralHardBlockReasons / maxAllowedQuality.
189
+ - Use this context as the explicit strategy-specific summary instead of trying to derive the same conclusion again only from generic candles.
190
+ - If \`payload.additionalIndicators.derivativesContext\` exists, it is a Coinalyze-derived summary of derivatives state at signal time; \`stale\` or \`missing_derivatives\` means that Coinalyze context must not be used.
191
+ `;
192
+ var toFiniteNumberOrNull = (value) => {
193
+ if (typeof value === "number" && Number.isFinite(value)) {
194
+ return value;
195
+ }
196
+ if (typeof value === "string" && value.trim()) {
197
+ const parsed = Number(value);
198
+ return Number.isFinite(parsed) ? parsed : null;
199
+ }
200
+ return null;
201
+ };
202
+ var getLastFiniteNumber = (value) => {
203
+ if (Array.isArray(value)) {
204
+ for (let i = value.length - 1; i >= 0; i -= 1) {
205
+ const item = toFiniteNumberOrNull(value[i]);
206
+ if (item != null) {
207
+ return item;
208
+ }
209
+ }
210
+ return null;
211
+ }
212
+ return toFiniteNumberOrNull(value);
213
+ };
214
+ var getBias = (fast, slow) => {
215
+ if (fast == null || slow == null) {
216
+ return null;
217
+ }
218
+ if (fast > slow) {
219
+ return "bullish";
220
+ }
221
+ if (fast < slow) {
222
+ return "bearish";
223
+ }
224
+ return null;
225
+ };
226
+ var getSignalDirection = (signal) => signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
227
+ var getDivergenceSummary = (signal) => {
228
+ const additional = signal.additionalIndicators;
229
+ const divergence = additional?.divergence;
230
+ return divergence && typeof divergence === "object" ? divergence : {};
231
+ };
232
+ var getVolumeDivergenceSetupSummary = (signal) => {
233
+ const additional = signal.additionalIndicators;
234
+ const setup = additional?.volumeDivergenceSetup;
235
+ return setup && typeof setup === "object" ? setup : {};
236
+ };
237
+ var getVolumeDivergenceThresholdSummary = (signal) => {
238
+ const additional = signal.additionalIndicators;
239
+ const thresholds = additional?.volumeDivergenceThresholds;
240
+ if (!thresholds || typeof thresholds !== "object") {
241
+ return DEFAULT_VOLUME_DIVERGENCE_ENTRY_THRESHOLDS;
242
+ }
243
+ const candidate = thresholds;
244
+ return {
245
+ allowStructureAdvanceEntry: typeof candidate.allowStructureAdvanceEntry === "boolean" ? candidate.allowStructureAdvanceEntry : DEFAULT_VOLUME_DIVERGENCE_ENTRY_THRESHOLDS.allowStructureAdvanceEntry,
246
+ minDivergenceAmplitudeAtrRatio: toFiniteNumberOrNull(candidate.minDivergenceAmplitudeAtrRatio) ?? DEFAULT_VOLUME_DIVERGENCE_ENTRY_THRESHOLDS.minDivergenceAmplitudeAtrRatio,
247
+ minReclaimPct: toFiniteNumberOrNull(candidate.minReclaimPct) ?? DEFAULT_VOLUME_DIVERGENCE_ENTRY_THRESHOLDS.minReclaimPct,
248
+ minConfirmationCandleQuality: toFiniteNumberOrNull(candidate.minConfirmationCandleQuality) ?? DEFAULT_VOLUME_DIVERGENCE_ENTRY_THRESHOLDS.minConfirmationCandleQuality
249
+ };
250
+ };
251
+ var isAtLeast = (value, threshold) => value != null && value >= threshold;
252
+ var isAtMost = (value, threshold) => value != null && value <= threshold;
253
+ var isInRange = (value, min, max) => value != null && value >= min && value <= max;
254
+ var getConfirmationDistancePct = ({
255
+ signalDirection,
256
+ currentPrice,
257
+ confirmationPrice,
258
+ setupValue
259
+ }) => {
260
+ if (setupValue != null) {
261
+ return setupValue;
262
+ }
263
+ if (signalDirection == null || currentPrice == null || confirmationPrice == null || confirmationPrice <= 0) {
264
+ return null;
265
+ }
266
+ return signalDirection === "LONG" ? (currentPrice - confirmationPrice) / confirmationPrice * 100 : (confirmationPrice - currentPrice) / confirmationPrice * 100;
267
+ };
268
+ var buildHardBlockReasons = ({
269
+ confirmationReady,
270
+ reboundFromPivotPct,
271
+ divergenceAmplitudeAtrRatio,
272
+ reclaimPct,
273
+ confirmationCandleQuality,
274
+ entryThresholds
275
+ }) => {
276
+ const reasons = [];
277
+ if (reboundFromPivotPct != null && reboundFromPivotPct <= 0) {
278
+ reasons.push("no_rebound_from_pivot");
279
+ }
280
+ if (divergenceAmplitudeAtrRatio != null && divergenceAmplitudeAtrRatio < entryThresholds.minDivergenceAmplitudeAtrRatio) {
281
+ reasons.push("weak_divergence_amplitude");
282
+ }
283
+ if (confirmationReady && reclaimPct != null && reclaimPct < entryThresholds.minReclaimPct) {
284
+ reasons.push("weak_reclaim");
285
+ }
286
+ if (confirmationReady && confirmationCandleQuality != null && confirmationCandleQuality < entryThresholds.minConfirmationCandleQuality) {
287
+ reasons.push("weak_confirmation_candle");
288
+ }
289
+ return reasons;
290
+ };
291
+ var getLongQ4Demotion = ({
292
+ divergenceAmplitudeAtrRatio,
293
+ volumeDivergenceRatio,
294
+ coinBiasAligned,
295
+ btcBiasAligned,
296
+ confirmationDistancePct,
297
+ barsSinceDetection,
298
+ atrPct,
299
+ reclaimPct
300
+ }) => {
301
+ const longOverextendedWithoutVolumeSupport = isAtLeast(divergenceAmplitudeAtrRatio, 2.2) && volumeDivergenceRatio != null && volumeDivergenceRatio < 2.2;
302
+ const longBtcLedWithoutCoinSupport = coinBiasAligned === false && btcBiasAligned === true && volumeDivergenceRatio != null && volumeDivergenceRatio < 2.6;
303
+ const longDoubleConflictWithoutMaturity = coinBiasAligned === false && btcBiasAligned === false && (!isAtLeast(confirmationDistancePct, 0.35) || !isAtLeast(barsSinceDetection, 2) || !isAtMost(atrPct, 0.95));
304
+ const longDoubleConflictOverextended = coinBiasAligned === false && btcBiasAligned === false && (!isAtMost(confirmationDistancePct, 1.4) || !isAtMost(divergenceAmplitudeAtrRatio, 2.2) || !isAtMost(atrPct, 1) || !isAtLeast(reclaimPct, 130));
305
+ const longDoubleConflictStaleConfirmation = coinBiasAligned === false && btcBiasAligned === false && isAtLeast(barsSinceDetection, 5);
306
+ const longFullyAlignedLateWeakConfirmation = coinBiasAligned === true && btcBiasAligned === true && isAtLeast(barsSinceDetection, 5) && (!isAtLeast(confirmationDistancePct, 1.2) || !isAtLeast(reclaimPct, 170));
307
+ const longLateExtendedConfirmation = isAtLeast(barsSinceDetection, 6) && isAtLeast(confirmationDistancePct, 1.5) && !isAtMost(atrPct, 1);
308
+ return longOverextendedWithoutVolumeSupport || longBtcLedWithoutCoinSupport || longDoubleConflictWithoutMaturity || longDoubleConflictOverextended || longDoubleConflictStaleConfirmation || longFullyAlignedLateWeakConfirmation || longLateExtendedConfirmation;
309
+ };
310
+ var getLongDeterministicQuality = ({
311
+ confirmationReady,
312
+ structureAdvanced,
313
+ hardBlockReasons,
314
+ divergenceAmplitudeAtrRatio,
315
+ reclaimPct,
316
+ confirmationCandleQuality,
317
+ atrPct,
318
+ confirmationDistancePct,
319
+ reboundFromPivotPct,
320
+ volumeDivergenceStrength,
321
+ volumeDivergenceRatio,
322
+ deltaAligned,
323
+ barsSinceDetection,
324
+ coinBiasAligned,
325
+ btcBiasAligned,
326
+ entryThresholds,
327
+ aiThresholds
328
+ }) => {
329
+ if (hardBlockReasons.length > 0) {
330
+ return 2;
331
+ }
332
+ const reboundModerate = isAtLeast(reboundFromPivotPct, 0.6);
333
+ const reboundStrong = isAtLeast(reboundFromPivotPct, 1.2);
334
+ const reboundVeryStrong = isAtLeast(reboundFromPivotPct, 1.8);
335
+ const confirmationDistanceModerate = isAtLeast(confirmationDistancePct, 0.35);
336
+ const confirmationDistanceStrong = isAtLeast(confirmationDistancePct, 0.7);
337
+ const confirmationDistanceContained = isAtMost(confirmationDistancePct, 1.4);
338
+ const confirmationDistanceBalanced = isInRange(
339
+ confirmationDistancePct,
340
+ 0.45,
341
+ 1.1
342
+ );
343
+ const maturityReady = isAtLeast(barsSinceDetection, 2);
344
+ const maturityFresh = isInRange(barsSinceDetection, 2, 5);
345
+ const maturityCounterTrend = isInRange(barsSinceDetection, 2, 4);
346
+ const calmAtr = isAtMost(atrPct, 0.95);
347
+ const veryCalmAtr = isAtMost(atrPct, 0.85);
348
+ const volumeModerate = isAtLeast(volumeDivergenceStrength, 5);
349
+ const volumeStrong = isAtLeast(volumeDivergenceStrength, 15);
350
+ const volumeVeryStrong = isAtLeast(volumeDivergenceStrength, 30);
351
+ const volumeRatioModerate = isAtLeast(volumeDivergenceRatio, 1.3);
352
+ const volumeRatioStrong = isAtLeast(volumeDivergenceRatio, 1.7);
353
+ const volumeRatioVeryStrong = isAtLeast(volumeDivergenceRatio, 2.2);
354
+ const longBiasConflictCount = Number(coinBiasAligned === false) + Number(btcBiasAligned === false);
355
+ const longQ4Demotion = getLongQ4Demotion({
356
+ divergenceAmplitudeAtrRatio,
357
+ volumeDivergenceRatio,
358
+ coinBiasAligned,
359
+ btcBiasAligned,
360
+ confirmationDistancePct,
361
+ barsSinceDetection,
362
+ atrPct,
363
+ reclaimPct
364
+ });
365
+ const q4SetupReady = aiThresholds != null && isAtLeast(
366
+ divergenceAmplitudeAtrRatio,
367
+ aiThresholds.q4DivergenceAmplitudeAtrRatio
368
+ ) && isAtLeast(reclaimPct, aiThresholds.q4ReclaimPct) && isAtLeast(
369
+ confirmationCandleQuality,
370
+ aiThresholds.q4ConfirmationCandleQuality
371
+ );
372
+ const q5SetupReady = aiThresholds != null && isAtLeast(
373
+ divergenceAmplitudeAtrRatio,
374
+ aiThresholds.q5DivergenceAmplitudeAtrRatio
375
+ ) && isAtLeast(reclaimPct, aiThresholds.q5ReclaimPct) && isAtLeast(
376
+ confirmationCandleQuality,
377
+ aiThresholds.q5ConfirmationCandleQuality
378
+ );
379
+ const minimumSetupReady = isAtLeast(
380
+ divergenceAmplitudeAtrRatio,
381
+ entryThresholds.minDivergenceAmplitudeAtrRatio
382
+ ) && isAtLeast(reclaimPct, entryThresholds.minReclaimPct) && isAtLeast(
383
+ confirmationCandleQuality,
384
+ entryThresholds.minConfirmationCandleQuality
385
+ );
386
+ const longSelectivePromotion = confirmationReady && minimumSetupReady && reboundStrong && confirmationDistanceBalanced && maturityFresh && calmAtr && volumeStrong && volumeRatioStrong && isAtLeast(reclaimPct, Math.max(entryThresholds.minReclaimPct + 15, 130)) && isAtLeast(
387
+ confirmationCandleQuality,
388
+ Math.max(entryThresholds.minConfirmationCandleQuality + 0.1, 0.7)
389
+ ) && deltaAligned !== false && longBiasConflictCount <= 1 && !longQ4Demotion;
390
+ const longAlignedFnPromotion = confirmationReady && minimumSetupReady && coinBiasAligned === true && btcBiasAligned === true && reboundModerate && calmAtr && isAtLeast(reclaimPct, 145) && isAtLeast(confirmationCandleQuality, 0.8) && isAtMost(divergenceAmplitudeAtrRatio, 1.8) && isAtMost(confirmationDistancePct, 0.8) && !longQ4Demotion;
391
+ const longSemiAlignedFnPromotion = confirmationReady && minimumSetupReady && (coinBiasAligned === true || btcBiasAligned === true) && reboundModerate && isAtLeast(reclaimPct, 140) && isAtLeast(confirmationCandleQuality, 0.8) && isAtMost(divergenceAmplitudeAtrRatio, 2.5) && !longQ4Demotion;
392
+ const longCounterTrendSelectivePromotion = confirmationReady && minimumSetupReady && reboundStrong && confirmationDistanceBalanced && maturityCounterTrend && veryCalmAtr && volumeVeryStrong && volumeRatioVeryStrong && isAtLeast(reclaimPct, 130) && isAtLeast(
393
+ confirmationCandleQuality,
394
+ Math.max(entryThresholds.minConfirmationCandleQuality + 0.1, 0.7)
395
+ ) && longBiasConflictCount === 2 && !longQ4Demotion;
396
+ if (confirmationReady && q5SetupReady && reboundVeryStrong && confirmationDistanceStrong && confirmationDistanceContained && maturityReady && calmAtr && volumeVeryStrong && volumeRatioVeryStrong && deltaAligned === true && longBiasConflictCount === 0 && !longQ4Demotion) {
397
+ return 5;
398
+ }
399
+ if (longCounterTrendSelectivePromotion || longSemiAlignedFnPromotion || longAlignedFnPromotion || longSelectivePromotion || confirmationReady && q4SetupReady && reboundModerate && confirmationDistanceModerate && confirmationDistanceContained && volumeModerate && volumeRatioModerate && !longQ4Demotion && deltaAligned !== false) {
400
+ return 4;
401
+ }
402
+ if (confirmationReady && minimumSetupReady && reboundModerate) {
403
+ return 3;
404
+ }
405
+ if (structureAdvanced && isAtLeast(reboundFromPivotPct, 0.25)) {
406
+ return 3;
407
+ }
408
+ return 2;
409
+ };
410
+ var getShortDeterministicQuality = ({
411
+ confirmationReady,
412
+ structureAdvanced,
413
+ hardBlockReasons,
414
+ divergenceAmplitudeAtrRatio,
415
+ reclaimPct,
416
+ confirmationCandleQuality,
417
+ reboundFromPivotPct,
418
+ volumeDivergenceStrength,
419
+ deltaAligned,
420
+ coinBiasAligned,
421
+ btcBiasAligned,
422
+ entryThresholds,
423
+ aiThresholds
424
+ }) => {
425
+ if (hardBlockReasons.length > 0) {
426
+ return 2;
427
+ }
428
+ const reboundModerate = isAtLeast(reboundFromPivotPct, 0.6);
429
+ const reboundStrong = isAtLeast(reboundFromPivotPct, 1.2);
430
+ const reboundVeryStrong = isAtLeast(reboundFromPivotPct, 1.8);
431
+ const volumeVeryStrong = isAtLeast(volumeDivergenceStrength, 30);
432
+ const shortBiasConflictCount = Number(coinBiasAligned === false) + Number(btcBiasAligned === false);
433
+ const q4SetupReady = aiThresholds != null && isAtLeast(
434
+ divergenceAmplitudeAtrRatio,
435
+ aiThresholds.q4DivergenceAmplitudeAtrRatio
436
+ ) && isAtLeast(reclaimPct, aiThresholds.q4ReclaimPct) && isAtLeast(
437
+ confirmationCandleQuality,
438
+ aiThresholds.q4ConfirmationCandleQuality
439
+ );
440
+ const q5SetupReady = aiThresholds != null && isAtLeast(
441
+ divergenceAmplitudeAtrRatio,
442
+ aiThresholds.q5DivergenceAmplitudeAtrRatio
443
+ ) && isAtLeast(reclaimPct, aiThresholds.q5ReclaimPct) && isAtLeast(
444
+ confirmationCandleQuality,
445
+ aiThresholds.q5ConfirmationCandleQuality
446
+ );
447
+ const minimumSetupReady = isAtLeast(
448
+ divergenceAmplitudeAtrRatio,
449
+ entryThresholds.minDivergenceAmplitudeAtrRatio
450
+ ) && isAtLeast(reclaimPct, entryThresholds.minReclaimPct) && isAtLeast(
451
+ confirmationCandleQuality,
452
+ entryThresholds.minConfirmationCandleQuality
453
+ );
454
+ if (confirmationReady && q5SetupReady && reboundVeryStrong && volumeVeryStrong && deltaAligned === true && shortBiasConflictCount === 0) {
455
+ return 5;
456
+ }
457
+ if (confirmationReady && q4SetupReady && reboundStrong && volumeVeryStrong && deltaAligned === true && shortBiasConflictCount === 0) {
458
+ return 4;
459
+ }
460
+ if (confirmationReady && minimumSetupReady && reboundModerate && deltaAligned !== false) {
461
+ return 3;
462
+ }
463
+ if (structureAdvanced && isAtLeast(reboundFromPivotPct, 0.25) && deltaAligned !== false) {
464
+ return 3;
465
+ }
466
+ return 2;
467
+ };
468
+ var getVolumeDivergenceContext = (signal) => {
469
+ const signalDirection = getSignalDirection(signal);
470
+ const divergence = getDivergenceSummary(signal);
471
+ const divergenceKind = divergence.kind === "bullish" || divergence.kind === "bearish" ? divergence.kind : null;
472
+ const currentPrice = toFiniteNumberOrNull(signal.prices?.currentPrice);
473
+ const currentPivotLow = toFiniteNumberOrNull(
474
+ divergence.currentPivot?.priceLow
475
+ );
476
+ const currentPivotHigh = toFiniteNumberOrNull(
477
+ divergence.currentPivot?.priceHigh
478
+ );
479
+ const previousPivotLow = toFiniteNumberOrNull(
480
+ divergence.previousPivot?.priceLow
481
+ );
482
+ const previousPivotHigh = toFiniteNumberOrNull(
483
+ divergence.previousPivot?.priceHigh
484
+ );
485
+ const currentVolumeNorm = toFiniteNumberOrNull(
486
+ divergence.currentPivot?.volumeNorm
487
+ );
488
+ const previousVolumeNorm = toFiniteNumberOrNull(
489
+ divergence.previousPivot?.volumeNorm
490
+ );
491
+ const pivotLookbackRight = toFiniteNumberOrNull(
492
+ divergence.pivotLookbackRight
493
+ );
494
+ const barsBetweenPivotConfirmations = toFiniteNumberOrNull(
495
+ divergence.barsBetweenPivotConfirmations
496
+ );
497
+ const timing = signal.additionalIndicators?.volumeDivergenceSignalTiming;
498
+ const entryTiming = timing?.entryTiming === "confirmation_ready" || timing?.entryTiming === "structure_advance" ? timing.entryTiming : null;
499
+ const barsSinceDetection = toFiniteNumberOrNull(timing?.barsSinceDetection);
500
+ const deltaAtPivot = toFiniteNumberOrNull(
501
+ signal.additionalIndicators?.deltaAtPivot
502
+ );
503
+ const setup = getVolumeDivergenceSetupSummary(signal);
504
+ const atrPct = toFiniteNumberOrNull(setup.atrPct);
505
+ const divergenceAmplitudeAtrRatio = toFiniteNumberOrNull(
506
+ setup.divergenceAmplitudeAtrRatio
507
+ );
508
+ const reclaimPct = toFiniteNumberOrNull(setup.reclaimPct);
509
+ const confirmationCandleQuality = toFiniteNumberOrNull(
510
+ setup.confirmationCandleQuality
511
+ );
512
+ const setupConfirmationDistancePct = toFiniteNumberOrNull(
513
+ setup.confirmationDistancePct
514
+ );
515
+ const entryThresholds = getVolumeDivergenceThresholdSummary(signal);
516
+ const coinMaBias = getBias(
517
+ getLastFiniteNumber(signal.indicators?.maFast),
518
+ getLastFiniteNumber(signal.indicators?.maSlow)
519
+ );
520
+ const btcMaBias = getBias(
521
+ getLastFiniteNumber(signal.indicators?.btcMaFast),
522
+ getLastFiniteNumber(signal.indicators?.btcMaSlow)
523
+ );
524
+ const confirmationPrice = divergenceKind === "bullish" ? currentPivotHigh : divergenceKind === "bearish" ? currentPivotLow : null;
525
+ const confirmationReady = divergenceKind === "bullish" ? currentPrice != null && confirmationPrice != null && currentPrice >= confirmationPrice : divergenceKind === "bearish" ? currentPrice != null && confirmationPrice != null && currentPrice <= confirmationPrice : false;
526
+ const confirmationDistancePct = getConfirmationDistancePct({
527
+ signalDirection,
528
+ currentPrice,
529
+ confirmationPrice,
530
+ setupValue: setupConfirmationDistancePct
531
+ });
532
+ const structureAdvanced = divergenceKind === "bullish" ? currentPrice != null && previousPivotLow != null && currentPrice >= previousPivotLow : divergenceKind === "bearish" ? currentPrice != null && previousPivotHigh != null && currentPrice <= previousPivotHigh : false;
533
+ const reboundFromPivotPct = divergenceKind === "bullish" && currentPrice != null && currentPivotLow != null && currentPivotLow > 0 ? (currentPrice - currentPivotLow) / currentPivotLow * 100 : divergenceKind === "bearish" && currentPrice != null && currentPivotHigh != null && currentPivotHigh > 0 ? (currentPivotHigh - currentPrice) / currentPivotHigh * 100 : null;
534
+ const priceDisplacementPct = divergenceKind === "bullish" && currentPivotLow != null && previousPivotLow != null && previousPivotLow > 0 ? (previousPivotLow - currentPivotLow) / previousPivotLow * 100 : divergenceKind === "bearish" && currentPivotHigh != null && previousPivotHigh != null && previousPivotHigh > 0 ? (currentPivotHigh - previousPivotHigh) / previousPivotHigh * 100 : null;
535
+ const volumeDivergenceStrength = divergenceKind === "bullish" && currentVolumeNorm != null && previousVolumeNorm != null ? currentVolumeNorm - previousVolumeNorm : divergenceKind === "bearish" && currentVolumeNorm != null && previousVolumeNorm != null ? previousVolumeNorm - currentVolumeNorm : null;
536
+ const volumeDivergenceRatio = divergenceKind === "bullish" && currentVolumeNorm != null && previousVolumeNorm != null && previousVolumeNorm > 0 ? currentVolumeNorm / previousVolumeNorm : divergenceKind === "bearish" && currentVolumeNorm != null && previousVolumeNorm != null && currentVolumeNorm > 0 ? previousVolumeNorm / currentVolumeNorm : null;
537
+ const deltaAligned = signalDirection === "LONG" ? deltaAtPivot != null ? deltaAtPivot > 0 : null : signalDirection === "SHORT" ? deltaAtPivot != null ? deltaAtPivot < 0 : null : null;
538
+ const coinBiasAligned = signalDirection === "LONG" ? coinMaBias != null ? coinMaBias === "bullish" : null : signalDirection === "SHORT" ? coinMaBias != null ? coinMaBias === "bearish" : null : null;
539
+ const btcBiasAligned = signalDirection === "LONG" ? btcMaBias != null ? btcMaBias === "bullish" : null : signalDirection === "SHORT" ? btcMaBias != null ? btcMaBias === "bearish" : null : null;
540
+ const aiThresholds = signalDirection != null ? getVolumeDivergenceAiThresholds(signalDirection) : null;
541
+ const hardBlockReasons = buildHardBlockReasons({
542
+ confirmationReady,
543
+ reboundFromPivotPct,
544
+ divergenceAmplitudeAtrRatio,
545
+ reclaimPct,
546
+ confirmationCandleQuality,
547
+ entryThresholds
548
+ });
549
+ const deterministicQuality = signalDirection === "LONG" ? getLongDeterministicQuality({
550
+ confirmationReady,
551
+ structureAdvanced,
552
+ hardBlockReasons,
553
+ divergenceAmplitudeAtrRatio,
554
+ reclaimPct,
555
+ confirmationCandleQuality,
556
+ atrPct,
557
+ confirmationDistancePct,
558
+ reboundFromPivotPct,
559
+ volumeDivergenceStrength,
560
+ volumeDivergenceRatio,
561
+ deltaAligned,
562
+ barsSinceDetection,
563
+ coinBiasAligned,
564
+ btcBiasAligned,
565
+ entryThresholds,
566
+ aiThresholds
567
+ }) : signalDirection === "SHORT" ? getShortDeterministicQuality({
568
+ confirmationReady,
569
+ structureAdvanced,
570
+ hardBlockReasons,
571
+ divergenceAmplitudeAtrRatio,
572
+ reclaimPct,
573
+ confirmationCandleQuality,
574
+ reboundFromPivotPct,
575
+ volumeDivergenceStrength,
576
+ deltaAligned,
577
+ coinBiasAligned,
578
+ btcBiasAligned,
579
+ entryThresholds,
580
+ aiThresholds
581
+ }) : hardBlockReasons.length > 0 ? 2 : 3;
582
+ const approvalAllowedNow = hardBlockReasons.length === 0 && deterministicQuality >= 4 && confirmationReady;
583
+ return {
584
+ signalDirection,
585
+ divergenceKind,
586
+ confirmationPrice,
587
+ confirmationReady,
588
+ structureAdvanced,
589
+ reboundFromPivotPct,
590
+ confirmationDistancePct,
591
+ priceDisplacementPct,
592
+ atrPct,
593
+ divergenceAmplitudeAtrRatio,
594
+ reclaimPct,
595
+ confirmationCandleQuality,
596
+ volumeDivergenceStrength,
597
+ volumeDivergenceRatio,
598
+ deltaAtPivot,
599
+ deltaAligned,
600
+ barsSincePivot: pivotLookbackRight,
601
+ barsBetweenPivotConfirmations,
602
+ entryTiming,
603
+ barsSinceDetection,
604
+ coinMaBias,
605
+ btcMaBias,
606
+ coinBiasAligned,
607
+ btcBiasAligned,
608
+ hardBlockReasons,
609
+ structuralHardBlockReasons: [...hardBlockReasons],
610
+ deterministicQuality,
611
+ approvalAllowedNow,
612
+ maxAllowedQuality: deterministicQuality
613
+ };
614
+ };
615
+ var getVolumeDivergenceContextFromPayload = (payload, signal) => {
616
+ const additional = payload.additionalIndicators;
617
+ const fromPayload = additional?.volumeDivergenceContext;
618
+ if (fromPayload && typeof fromPayload === "object") {
619
+ return fromPayload;
620
+ }
621
+ return getVolumeDivergenceContext(signal);
622
+ };
623
+ var clampQuality = (quality, maxAllowedQuality) => Math.max(1, Math.min(5, Math.min(quality, maxAllowedQuality)));
624
+ var getHardBlockReasonText = (reason) => {
625
+ switch (reason) {
626
+ case "no_rebound_from_pivot":
627
+ return "price failed to move away from the pivot in the reversal direction";
628
+ case "weak_divergence_amplitude":
629
+ return "divergence amplitude is too small relative to ATR";
630
+ case "weak_reclaim":
631
+ return "price reclaimed too little structure after the pivot";
632
+ case "weak_confirmation_candle":
633
+ return "confirmation candle is too weak";
634
+ default:
635
+ return reason;
636
+ }
637
+ };
638
+ var buildGuardrailReason = (context) => {
639
+ if (context.hardBlockReasons.length > 0) {
640
+ return `VolumeDivergence guardrail: ${context.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.`;
641
+ }
642
+ if (!context.confirmationReady && context.entryTiming == null) {
643
+ return "VolumeDivergence guardrail: reversal is visible, but the confirmation level has not been cleared yet.";
644
+ }
645
+ return "VolumeDivergence guardrail: quality is limited by confirmation state and the strength of reversal away from the pivot.";
646
+ };
647
+ var postProcessAnalysis = ({
648
+ signal,
649
+ payload,
650
+ analysis
651
+ }) => {
652
+ const context = getVolumeDivergenceContextFromPayload(payload, signal);
653
+ const signalDirection = getSignalDirection(signal);
654
+ const requestedDirection = analysis.direction === signalDirection ? signalDirection : null;
655
+ const finalDirection = requestedDirection != null && context.approvalAllowedNow ? requestedDirection : null;
656
+ const requestedQuality = typeof analysis.quality === "number" ? analysis.quality : context.maxAllowedQuality;
657
+ const finalQuality = clampQuality(
658
+ requestedQuality,
659
+ context.maxAllowedQuality
660
+ );
661
+ const needRetest = finalDirection == null;
662
+ const retestPrice = needRetest ? context.confirmationPrice : null;
663
+ if (finalDirection == null) {
664
+ return {
665
+ ...analysis,
666
+ direction: null,
667
+ quality: finalQuality,
668
+ needRetest,
669
+ retestPrice,
670
+ takeProfitPrice: null,
671
+ stopLossPrice: null,
672
+ qualityReason: analysis.qualityReason || buildGuardrailReason(context),
673
+ triggerInvalidation: analysis.triggerInvalidation || (context.confirmationPrice != null ? `Wait for reversal confirmation relative to level ${context.confirmationPrice}.` : "Wait for a confirmed reversal after the pivot."),
674
+ comment: analysis.comment || (context.hardBlockReasons.length > 0 ? `VolumeDivergence rejected: ${context.hardBlockReasons.map(getHardBlockReasonText).join("; ")}.` : "VolumeDivergence remains in watch mode until the reversal is confirmed.")
675
+ };
676
+ }
677
+ return {
678
+ ...analysis,
679
+ direction: finalDirection,
680
+ quality: finalQuality,
681
+ needRetest,
682
+ retestPrice,
683
+ takeProfitPrice: signal.prices?.takeProfitPrice ?? null,
684
+ stopLossPrice: signal.prices?.stopLossPrice ?? null
685
+ };
686
+ };
687
+ var volumeDivergenceAiAdapter = {
688
+ buildPayload: ({ signal, basePayload }) => ({
689
+ ...basePayload,
690
+ additionalIndicators: {
691
+ ...basePayload.additionalIndicators,
692
+ volumeDivergenceContext: getVolumeDivergenceContext(signal)
693
+ }
694
+ }),
695
+ postProcessAnalysis,
696
+ buildSystemPromptAddon: () => `${VOLUME_DIVERGENCE_CONTEXT_PROMPT}
697
+ ${VOLUME_DIVERGENCE_PAYLOAD_PROMPT}`,
698
+ buildHumanPromptAddon: ({ signal, payload }) => {
699
+ const context = getVolumeDivergenceContextFromPayload(payload, signal);
700
+ return `
701
+
702
+ Additional VolumeDivergence context:
703
+ - divergenceKind=${context.divergenceKind ?? "n/a"}
704
+ - confirmationPrice=${context.confirmationPrice ?? "n/a"}
705
+ - confirmationReady=${context.confirmationReady}
706
+ - structureAdvanced=${context.structureAdvanced}
707
+ - reboundFromPivotPct=${context.reboundFromPivotPct?.toFixed?.(3) ?? "n/a"}%
708
+ - atrPct=${context.atrPct?.toFixed?.(3) ?? "n/a"}%
709
+ - divergenceAmplitudeAtrRatio=${context.divergenceAmplitudeAtrRatio?.toFixed?.(3) ?? "n/a"}
710
+ - reclaimPct=${context.reclaimPct?.toFixed?.(3) ?? "n/a"}
711
+ - confirmationCandleQuality=${context.confirmationCandleQuality?.toFixed?.(3) ?? "n/a"}
712
+ - confirmationDistancePct=${context.confirmationDistancePct?.toFixed?.(3) ?? "n/a"}%
713
+ - priceDisplacementPct=${context.priceDisplacementPct?.toFixed?.(3) ?? "n/a"}%
714
+ - volumeDivergenceStrength=${context.volumeDivergenceStrength?.toFixed?.(3) ?? "n/a"}
715
+ - volumeDivergenceRatio=${context.volumeDivergenceRatio?.toFixed?.(3) ?? "n/a"}
716
+ - deltaAligned=${context.deltaAligned}
717
+ - coinBiasAligned=${context.coinBiasAligned}
718
+ - btcBiasAligned=${context.btcBiasAligned}
719
+ - barsSincePivot=${context.barsSincePivot ?? "n/a"}
720
+ - barsBetweenPivotConfirmations=${context.barsBetweenPivotConfirmations ?? "n/a"}
721
+ - entryTiming=${context.entryTiming ?? "n/a"}
722
+ - barsSinceDetection=${context.barsSinceDetection ?? "n/a"}
723
+ - deterministicQuality=${context.deterministicQuality}
724
+ - approvalAllowedNow=${context.approvalAllowedNow}
725
+ - structuralHardBlockReasons=${context.structuralHardBlockReasons.join(", ") || "none"}
726
+ - maxAllowedQuality=${context.maxAllowedQuality}
727
+ - hardBlockReasons=${context.hardBlockReasons.join(", ") || "none"}
728
+
729
+ Interpretation rules for VolumeDivergence:
730
+ - first evaluate whether there is a real reversal away from the pivot, not just divergence on paper;
731
+ - \`confirmationReady=false\` usually means the reversal is not fully confirmed yet;
732
+ - if price did not bounce away from the current pivot in the signal direction, do not treat the setup as confirmed;
733
+ - delta or bias conflict should reduce quality, not be ignored.
734
+ `;
735
+ },
736
+ mapEntryRuntimeFromConfig: (config2) => mapAiRuntimeFromConfig(
737
+ config2
738
+ )
739
+ };
740
+
741
+ // src/VolumeDivergence/adapters/ml.ts
742
+ import { mapMlRuntimeFromConfig } from "@tradejs/core/strategies";
743
+ var toVolumeDivergenceMlStrategyConfig = (input) => {
744
+ if (!input) return void 0;
745
+ return {
746
+ ...input,
747
+ VOLUME_DIVERGENCE_CONFIG: input.VOLUME_DIVERGENCE_CONFIG ?? {
748
+ normalizationLength: input.NORMALIZATION_LENGTH,
749
+ pivotLookbackLeft: input.PIVOT_LOOKBACK_LEFT,
750
+ pivotLookbackRight: input.PIVOT_LOOKBACK_RIGHT,
751
+ maxBarsBetweenPivots: input.MAX_BARS_BETWEEN_PIVOTS,
752
+ minBarsBetweenPivots: input.MIN_BARS_BETWEEN_PIVOTS,
753
+ bullish: input.BULLISH,
754
+ bearish: input.BEARISH
755
+ }
756
+ };
757
+ };
758
+ var volumeDivergenceMlAdapter = {
759
+ normalizeStrategyConfig: (strategyConfig) => {
760
+ return toVolumeDivergenceMlStrategyConfig(strategyConfig);
761
+ },
762
+ mapEntryRuntimeFromConfig: (config2) => mapMlRuntimeFromConfig(config2, {
763
+ strategyConfig: toVolumeDivergenceMlStrategyConfig(
764
+ config2
765
+ )
766
+ })
767
+ };
768
+
769
+ // src/VolumeDivergence/manifest.ts
770
+ var volumeDivergenceManifest = {
771
+ name: "VolumeDivergence",
772
+ aiAdapter: volumeDivergenceAiAdapter,
773
+ mlAdapter: volumeDivergenceMlAdapter
774
+ };
775
+
776
+ // src/VolumeDivergence/config.ts
777
+ var config = {
778
+ ENV: "BACKTEST",
779
+ INTERVAL: "15",
780
+ MAKE_ORDERS: true,
781
+ CLOSE_OPPOSITE_POSITIONS: false,
782
+ BACKTEST_PRICE_MODE: "mid",
783
+ AI_ENABLED: false,
784
+ AI_MODE: "llm",
785
+ ML_ENABLED: false,
786
+ ML_THRESHOLD: 0.1,
787
+ MIN_AI_QUALITY: 3,
788
+ FEE_PERCENT: 5e-3,
789
+ MAX_LOSS_VALUE: 10,
790
+ MA_FAST: 14,
791
+ MA_MEDIUM: 49,
792
+ MA_SLOW: 50,
793
+ OBV_SMA: 10,
794
+ ATR: 14,
795
+ ATR_PCT_SHORT: 7,
796
+ ATR_PCT_LONG: 30,
797
+ BB: 20,
798
+ BB_STD: 2,
799
+ MACD_FAST: 12,
800
+ MACD_SLOW: 26,
801
+ MACD_SIGNAL: 9,
802
+ LEVEL_LOOKBACK: 20,
803
+ LEVEL_DELAY: 2,
804
+ NORMALIZATION_LENGTH: 100,
805
+ PIVOT_LOOKBACK_LEFT: 8,
806
+ PIVOT_LOOKBACK_RIGHT: 3,
807
+ MIN_BARS_BETWEEN_PIVOTS: 4,
808
+ MAX_BARS_BETWEEN_PIVOTS: 36,
809
+ ALLOW_STRUCTURE_ADVANCE_ENTRY: false,
810
+ MIN_DIVERGENCE_AMPLITUDE_ATR_RATIO: 0.35,
811
+ MIN_RECLAIM_PCT: 105,
812
+ MIN_CONFIRMATION_CANDLE_QUALITY: 0.58,
813
+ BULLISH: {
814
+ enable: true,
815
+ direction: "LONG",
816
+ TP: 4,
817
+ SL: 1.3,
818
+ minRiskRatio: 2
819
+ },
820
+ BEARISH: {
821
+ enable: true,
822
+ direction: "SHORT",
823
+ TP: 4,
824
+ SL: 1.3,
825
+ minRiskRatio: 2
826
+ }
827
+ };
828
+
829
+ export {
830
+ getVolumeDivergenceEntryThresholds,
831
+ buildVolumeDivergenceSetupFeatures,
832
+ volumeDivergenceAiAdapter,
833
+ volumeDivergenceMlAdapter,
834
+ volumeDivergenceManifest,
835
+ config
836
+ };