@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,613 @@
1
+ import {
2
+ buildVolumeDivergenceSetupFeatures,
3
+ config,
4
+ getVolumeDivergenceEntryThresholds,
5
+ volumeDivergenceManifest
6
+ } from "./chunk-QVWMBYYM.mjs";
7
+ import "./chunk-HEBXNMVQ.mjs";
8
+
9
+ // src/VolumeDivergence/strategy.ts
10
+ import { createStrategyRuntime } from "@tradejs/node/strategies";
11
+
12
+ // src/VolumeDivergence/core.ts
13
+ import { round } from "@tradejs/core/math";
14
+
15
+ // src/VolumeDivergence/figures.ts
16
+ var buildVolumeDivergenceFigures = ({
17
+ kind,
18
+ previousPivotIndex,
19
+ currentPivotIndex,
20
+ previousPivotLow,
21
+ previousPivotHigh,
22
+ currentPivotLow,
23
+ currentPivotHigh,
24
+ fullData
25
+ }) => ({
26
+ lines: [
27
+ {
28
+ id: `volume-divergence-price-${kind}`,
29
+ kind: `volume_divergence_${kind}_price`,
30
+ points: [
31
+ {
32
+ timestamp: fullData[previousPivotIndex]?.timestamp ?? 0,
33
+ value: kind === "bullish" ? previousPivotLow : previousPivotHigh
34
+ },
35
+ {
36
+ timestamp: fullData[currentPivotIndex]?.timestamp ?? 0,
37
+ value: kind === "bullish" ? currentPivotLow : currentPivotHigh
38
+ }
39
+ ],
40
+ color: kind === "bullish" ? "#22c55e" : "#ef4444",
41
+ width: 2,
42
+ style: "dashed"
43
+ }
44
+ ],
45
+ points: [
46
+ {
47
+ id: `volume-divergence-pivots-${kind}`,
48
+ kind: `volume_divergence_${kind}_pivots`,
49
+ points: [
50
+ {
51
+ timestamp: fullData[previousPivotIndex]?.timestamp ?? 0,
52
+ value: kind === "bullish" ? previousPivotLow : previousPivotHigh
53
+ },
54
+ {
55
+ timestamp: fullData[currentPivotIndex]?.timestamp ?? 0,
56
+ value: kind === "bullish" ? currentPivotLow : currentPivotHigh
57
+ }
58
+ ],
59
+ color: kind === "bullish" ? "#22c55e" : "#ef4444",
60
+ radius: 4
61
+ }
62
+ ]
63
+ });
64
+
65
+ // src/VolumeDivergence/core.ts
66
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
67
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
68
+ var isOpenPosition = (position) => Boolean(
69
+ position && typeof position.price === "number" && Number.isFinite(position.price) && typeof position.qty === "number" && Number.isFinite(position.qty) && position.qty > 0 && (position.direction === "LONG" || position.direction === "SHORT")
70
+ );
71
+ var compactQueue = (queue) => {
72
+ if (queue.start <= 1024 || queue.start * 2 <= queue.indices.length) {
73
+ return;
74
+ }
75
+ queue.indices.splice(0, queue.start);
76
+ queue.start = 0;
77
+ };
78
+ var rebaseQueue = (queue, offset) => {
79
+ queue.indices = queue.indices.slice(queue.start).map((index) => index - offset).filter((index) => index >= 0);
80
+ queue.start = 0;
81
+ };
82
+ var rebaseConfirmedPivots = (state, offset) => {
83
+ state.indices = state.indices.map((index) => index - offset).filter((index) => index >= 0);
84
+ state.nextConfirmationIndex = Math.max(
85
+ 0,
86
+ state.nextConfirmationIndex - offset
87
+ );
88
+ };
89
+ var appendNormalizedVolumes = ({
90
+ candles,
91
+ length,
92
+ normalizedVolumes,
93
+ queue
94
+ }) => {
95
+ while (normalizedVolumes.length < candles.length) {
96
+ const i = normalizedVolumes.length;
97
+ const windowStart = Math.max(0, i - length + 1);
98
+ const volume = Number(candles[i]?.volume) || 0;
99
+ while (queue.start < queue.indices.length && queue.indices[queue.start] < windowStart) {
100
+ queue.start += 1;
101
+ }
102
+ while (queue.indices.length > queue.start) {
103
+ const lastIndex = queue.indices[queue.indices.length - 1];
104
+ const lastVolume = Number(candles[lastIndex]?.volume) || 0;
105
+ if (lastVolume > volume) {
106
+ break;
107
+ }
108
+ queue.indices.pop();
109
+ }
110
+ queue.indices.push(i);
111
+ compactQueue(queue);
112
+ const highestIndex = queue.indices[queue.start];
113
+ const highest = Number(candles[highestIndex]?.volume) || 0;
114
+ normalizedVolumes.push(highest > 0 ? volume / highest * 100 : 0);
115
+ }
116
+ };
117
+ var isPivotHigh = ({
118
+ values,
119
+ index,
120
+ left,
121
+ right
122
+ }) => {
123
+ const pivotValue = values[index];
124
+ if (!isFiniteNumber(pivotValue)) {
125
+ return false;
126
+ }
127
+ if (index - left < 0 || index + right >= values.length) {
128
+ return false;
129
+ }
130
+ for (let i = index - left; i <= index + right; i += 1) {
131
+ if (i === index) continue;
132
+ if (!isFiniteNumber(values[i]) || values[i] >= pivotValue) {
133
+ return false;
134
+ }
135
+ }
136
+ return true;
137
+ };
138
+ var candleDeltaProxy = (candle) => {
139
+ const volume = Number(candle.volume) || 0;
140
+ const range = Math.max(Math.abs(candle.high - candle.low), 1e-9);
141
+ const bodyBias = (candle.close - candle.open) / range;
142
+ return volume * clamp(bodyBias, -1, 1);
143
+ };
144
+ var appendConfirmedPivotIndices = ({
145
+ candles,
146
+ normalizedVolumes,
147
+ lookbackLeft,
148
+ lookbackRight,
149
+ state
150
+ }) => {
151
+ const maxConfirmationIndex = candles.length - 1;
152
+ while (state.nextConfirmationIndex <= maxConfirmationIndex) {
153
+ const candidatePivotIndex = state.nextConfirmationIndex - lookbackRight;
154
+ if (candidatePivotIndex >= 0 && isPivotHigh({
155
+ values: normalizedVolumes,
156
+ index: candidatePivotIndex,
157
+ left: lookbackLeft,
158
+ right: lookbackRight
159
+ })) {
160
+ state.indices.push(candidatePivotIndex);
161
+ }
162
+ state.nextConfirmationIndex += 1;
163
+ }
164
+ };
165
+ var findLatestDivergence = ({
166
+ candles,
167
+ normalizedVolumes,
168
+ confirmedPivots,
169
+ lookbackLeft,
170
+ lookbackRight,
171
+ rangeLower,
172
+ rangeUpper
173
+ }) => {
174
+ const currentConfirmationIndex = candles.length - 1;
175
+ const currentPivotIndex = currentConfirmationIndex - lookbackRight;
176
+ if (currentPivotIndex <= 0) {
177
+ return null;
178
+ }
179
+ const lastConfirmedPivotIndex = confirmedPivots.length > 0 ? confirmedPivots[confirmedPivots.length - 1] : void 0;
180
+ if (lastConfirmedPivotIndex !== currentPivotIndex) {
181
+ return null;
182
+ }
183
+ const previousPivotIndex = confirmedPivots.length > 1 ? confirmedPivots[confirmedPivots.length - 2] : void 0;
184
+ if (previousPivotIndex == null || previousPivotIndex < lookbackLeft) {
185
+ return null;
186
+ }
187
+ const previousConfirmationIndex = previousPivotIndex + lookbackRight;
188
+ const barsBetweenPivotConfirmations = currentConfirmationIndex - previousConfirmationIndex - 1;
189
+ if (barsBetweenPivotConfirmations < rangeLower || barsBetweenPivotConfirmations > rangeUpper) {
190
+ return null;
191
+ }
192
+ const currentPivotVolumeNorm = normalizedVolumes[currentPivotIndex];
193
+ const previousPivotVolumeNorm = normalizedVolumes[previousPivotIndex];
194
+ const currentPivotLow = Number(candles[currentPivotIndex]?.low);
195
+ const previousPivotLow = Number(candles[previousPivotIndex]?.low);
196
+ const currentPivotHigh = Number(candles[currentPivotIndex]?.high);
197
+ const previousPivotHigh = Number(candles[previousPivotIndex]?.high);
198
+ const currentPivotCandle = candles[currentPivotIndex];
199
+ if (!isFiniteNumber(currentPivotVolumeNorm) || !isFiniteNumber(previousPivotVolumeNorm) || !isFiniteNumber(currentPivotLow) || !isFiniteNumber(previousPivotLow) || !isFiniteNumber(currentPivotHigh) || !isFiniteNumber(previousPivotHigh)) {
200
+ return null;
201
+ }
202
+ const volHigherLow = currentPivotVolumeNorm > previousPivotVolumeNorm;
203
+ const volLowerHigh = currentPivotVolumeNorm < previousPivotVolumeNorm;
204
+ const priceLowerLow = currentPivotLow < previousPivotLow;
205
+ const priceHigherHigh = currentPivotHigh > previousPivotHigh;
206
+ if (priceLowerLow && volHigherLow) {
207
+ return {
208
+ currentPivotIndex,
209
+ previousPivotIndex,
210
+ currentPivotVolumeNorm,
211
+ previousPivotVolumeNorm,
212
+ currentPivotLow,
213
+ previousPivotLow,
214
+ currentPivotHigh,
215
+ previousPivotHigh,
216
+ currentPivotVolume: Number(currentPivotCandle.volume) || 0,
217
+ currentPivotDelta: candleDeltaProxy(currentPivotCandle),
218
+ barsBetweenPivotConfirmations,
219
+ kind: "bullish"
220
+ };
221
+ }
222
+ if (priceHigherHigh && volLowerHigh) {
223
+ return {
224
+ currentPivotIndex,
225
+ previousPivotIndex,
226
+ currentPivotVolumeNorm,
227
+ previousPivotVolumeNorm,
228
+ currentPivotLow,
229
+ previousPivotLow,
230
+ currentPivotHigh,
231
+ previousPivotHigh,
232
+ currentPivotVolume: Number(currentPivotCandle.volume) || 0,
233
+ currentPivotDelta: candleDeltaProxy(currentPivotCandle),
234
+ barsBetweenPivotConfirmations,
235
+ kind: "bearish"
236
+ };
237
+ }
238
+ return null;
239
+ };
240
+ var getRequiredHistorySize = ({
241
+ normalizationLength,
242
+ lookbackLeft,
243
+ lookbackRight,
244
+ maxBarsBetweenPivots
245
+ }) => normalizationLength + maxBarsBetweenPivots + lookbackLeft + lookbackRight * 2 + 8;
246
+ var buildPendingDivergenceCandidate = ({
247
+ divergence,
248
+ candleWindow,
249
+ direction,
250
+ pivotLookbackLeft,
251
+ pivotLookbackRight,
252
+ detectedAtTimestamp
253
+ }) => ({
254
+ kind: divergence.kind,
255
+ direction,
256
+ currentPivotVolumeNorm: divergence.currentPivotVolumeNorm,
257
+ previousPivotVolumeNorm: divergence.previousPivotVolumeNorm,
258
+ currentPivotLow: divergence.currentPivotLow,
259
+ previousPivotLow: divergence.previousPivotLow,
260
+ currentPivotHigh: divergence.currentPivotHigh,
261
+ previousPivotHigh: divergence.previousPivotHigh,
262
+ currentPivotVolume: divergence.currentPivotVolume,
263
+ currentPivotDelta: divergence.currentPivotDelta,
264
+ barsBetweenPivotConfirmations: divergence.barsBetweenPivotConfirmations,
265
+ currentPivotTimestamp: Number(candleWindow[divergence.currentPivotIndex]?.timestamp) || null,
266
+ previousPivotTimestamp: Number(candleWindow[divergence.previousPivotIndex]?.timestamp) || null,
267
+ pivotLookbackLeft,
268
+ pivotLookbackRight,
269
+ detectedAtTimestamp,
270
+ lastObservedTimestamp: detectedAtTimestamp,
271
+ barsSinceDetection: 0
272
+ });
273
+ var updatePendingCandidateProgress = (candidate, timestamp) => {
274
+ if (candidate.lastObservedTimestamp === timestamp) {
275
+ return;
276
+ }
277
+ candidate.barsSinceDetection += 1;
278
+ candidate.lastObservedTimestamp = timestamp;
279
+ };
280
+ var resolvePendingEntryTiming = ({
281
+ candidate,
282
+ currentPrice
283
+ }) => {
284
+ if (candidate.direction === "LONG") {
285
+ if (currentPrice >= candidate.currentPivotHigh) {
286
+ return "confirmation_ready";
287
+ }
288
+ if (currentPrice >= candidate.previousPivotLow) {
289
+ return "structure_advance";
290
+ }
291
+ return null;
292
+ }
293
+ if (currentPrice <= candidate.currentPivotLow) {
294
+ return "confirmation_ready";
295
+ }
296
+ if (currentPrice <= candidate.previousPivotHigh) {
297
+ return "structure_advance";
298
+ }
299
+ return null;
300
+ };
301
+ var findCandleIndexByTimestamp = (candles, timestamp, fallbackIndex) => {
302
+ if (timestamp == null) {
303
+ return fallbackIndex;
304
+ }
305
+ const index = candles.findIndex(
306
+ (candle) => Number(candle.timestamp) === timestamp
307
+ );
308
+ return index >= 0 ? index : fallbackIndex;
309
+ };
310
+ var getModeConfigByKind = ({
311
+ kind,
312
+ bullish,
313
+ bearish
314
+ }) => kind === "bullish" ? bullish : bearish;
315
+ var buildEntryPayloadFromPendingCandidate = ({
316
+ candidate,
317
+ candleWindow,
318
+ entryTiming,
319
+ setupFeatures,
320
+ entryThresholds
321
+ }) => {
322
+ const previousPivotIndex = findCandleIndexByTimestamp(
323
+ candleWindow,
324
+ candidate.previousPivotTimestamp,
325
+ 0
326
+ );
327
+ const currentPivotIndex = findCandleIndexByTimestamp(
328
+ candleWindow,
329
+ candidate.currentPivotTimestamp,
330
+ candleWindow.length - 1
331
+ );
332
+ return {
333
+ figures: buildVolumeDivergenceFigures({
334
+ kind: candidate.kind,
335
+ previousPivotIndex,
336
+ currentPivotIndex,
337
+ previousPivotLow: candidate.previousPivotLow,
338
+ previousPivotHigh: candidate.previousPivotHigh,
339
+ currentPivotLow: candidate.currentPivotLow,
340
+ currentPivotHigh: candidate.currentPivotHigh,
341
+ fullData: candleWindow
342
+ }),
343
+ additionalIndicators: {
344
+ divergenceKind: candidate.kind,
345
+ normalizedVolumeAtPivot: candidate.currentPivotVolumeNorm,
346
+ previousNormalizedVolumeAtPivot: candidate.previousPivotVolumeNorm,
347
+ volumeAtPivot: candidate.currentPivotVolume,
348
+ deltaAtPivot: candidate.currentPivotDelta,
349
+ barsBetweenPivotConfirmations: candidate.barsBetweenPivotConfirmations,
350
+ divergence: {
351
+ kind: candidate.kind,
352
+ pivotLookbackLeft: candidate.pivotLookbackLeft,
353
+ pivotLookbackRight: candidate.pivotLookbackRight,
354
+ currentPivot: {
355
+ index: currentPivotIndex,
356
+ timestamp: candidate.currentPivotTimestamp,
357
+ priceLow: candidate.currentPivotLow,
358
+ priceHigh: candidate.currentPivotHigh,
359
+ volumeNorm: candidate.currentPivotVolumeNorm
360
+ },
361
+ previousPivot: {
362
+ index: previousPivotIndex,
363
+ timestamp: candidate.previousPivotTimestamp,
364
+ priceLow: candidate.previousPivotLow,
365
+ priceHigh: candidate.previousPivotHigh,
366
+ volumeNorm: candidate.previousPivotVolumeNorm
367
+ },
368
+ barsBetweenPivotConfirmations: candidate.barsBetweenPivotConfirmations
369
+ },
370
+ volumeDivergenceSignalTiming: {
371
+ entryTiming,
372
+ barsSinceDetection: candidate.barsSinceDetection,
373
+ detectedAtTimestamp: candidate.detectedAtTimestamp
374
+ },
375
+ volumeDivergenceSetup: setupFeatures,
376
+ volumeDivergenceThresholds: entryThresholds
377
+ }
378
+ };
379
+ };
380
+ var createVolumeDivergenceCore = async ({ config: config2, strategyApi, indicatorsState, data: initialData }) => {
381
+ const {
382
+ NORMALIZATION_LENGTH,
383
+ PIVOT_LOOKBACK_LEFT,
384
+ PIVOT_LOOKBACK_RIGHT,
385
+ MAX_BARS_BETWEEN_PIVOTS,
386
+ MIN_BARS_BETWEEN_PIVOTS,
387
+ ALLOW_STRUCTURE_ADVANCE_ENTRY,
388
+ MIN_DIVERGENCE_AMPLITUDE_ATR_RATIO,
389
+ MIN_RECLAIM_PCT,
390
+ MIN_CONFIRMATION_CANDLE_QUALITY,
391
+ FEE_PERCENT,
392
+ MAX_LOSS_VALUE,
393
+ BULLISH,
394
+ BEARISH
395
+ } = config2;
396
+ const entryThresholds = getVolumeDivergenceEntryThresholds({
397
+ ALLOW_STRUCTURE_ADVANCE_ENTRY,
398
+ MIN_DIVERGENCE_AMPLITUDE_ATR_RATIO,
399
+ MIN_RECLAIM_PCT,
400
+ MIN_CONFIRMATION_CANDLE_QUALITY
401
+ });
402
+ const lastTradeController = strategyApi.createLastTradeController();
403
+ const maxHistorySize = getRequiredHistorySize({
404
+ normalizationLength: NORMALIZATION_LENGTH,
405
+ lookbackLeft: PIVOT_LOOKBACK_LEFT,
406
+ lookbackRight: PIVOT_LOOKBACK_RIGHT,
407
+ maxBarsBetweenPivots: MAX_BARS_BETWEEN_PIVOTS
408
+ });
409
+ const maxPendingConfirmationBars = Math.max(
410
+ 2,
411
+ Math.min(
412
+ MAX_BARS_BETWEEN_PIVOTS,
413
+ PIVOT_LOOKBACK_LEFT + PIVOT_LOOKBACK_RIGHT + 1
414
+ )
415
+ );
416
+ const candleWindow = Array.isArray(initialData) ? initialData.slice(-maxHistorySize) : [];
417
+ const normalizedVolumes = [];
418
+ const rollingMaxQueue = {
419
+ indices: [],
420
+ start: 0
421
+ };
422
+ const confirmedPivotState = {
423
+ indices: [],
424
+ nextConfirmationIndex: 0
425
+ };
426
+ let pendingCandidate = null;
427
+ const syncDerivedState = () => {
428
+ appendNormalizedVolumes({
429
+ candles: candleWindow,
430
+ length: NORMALIZATION_LENGTH,
431
+ normalizedVolumes,
432
+ queue: rollingMaxQueue
433
+ });
434
+ appendConfirmedPivotIndices({
435
+ candles: candleWindow,
436
+ normalizedVolumes,
437
+ lookbackLeft: PIVOT_LOOKBACK_LEFT,
438
+ lookbackRight: PIVOT_LOOKBACK_RIGHT,
439
+ state: confirmedPivotState
440
+ });
441
+ };
442
+ const appendWindowCandle = (candle) => {
443
+ const latestTimestamp = candleWindow.length > 0 ? Number(candleWindow[candleWindow.length - 1]?.timestamp) : null;
444
+ if (latestTimestamp === Number(candle.timestamp)) {
445
+ candleWindow[candleWindow.length - 1] = candle;
446
+ normalizedVolumes.length = 0;
447
+ rollingMaxQueue.indices = [];
448
+ rollingMaxQueue.start = 0;
449
+ confirmedPivotState.indices = [];
450
+ confirmedPivotState.nextConfirmationIndex = 0;
451
+ syncDerivedState();
452
+ return;
453
+ }
454
+ candleWindow.push(candle);
455
+ if (candleWindow.length > maxHistorySize) {
456
+ const overflow = candleWindow.length - maxHistorySize;
457
+ candleWindow.splice(0, overflow);
458
+ normalizedVolumes.splice(0, overflow);
459
+ rebaseQueue(rollingMaxQueue, overflow);
460
+ rebaseConfirmedPivots(confirmedPivotState, overflow);
461
+ }
462
+ syncDerivedState();
463
+ };
464
+ syncDerivedState();
465
+ return async (candle) => {
466
+ appendWindowCandle(candle);
467
+ indicatorsState.onBar();
468
+ const timestamp = Number(candle.timestamp);
469
+ const currentPosition = await strategyApi.getCurrentPosition();
470
+ if (isOpenPosition(currentPosition)) {
471
+ return strategyApi.skip("POSITION_EXISTS");
472
+ }
473
+ if (candleWindow.length < PIVOT_LOOKBACK_LEFT + PIVOT_LOOKBACK_RIGHT + 2) {
474
+ return strategyApi.skip("WAIT_DATA");
475
+ }
476
+ if (lastTradeController.isInCooldown(timestamp)) {
477
+ return strategyApi.skip("DEV_TRADE_COOLDOWN");
478
+ }
479
+ const divergence = findLatestDivergence({
480
+ candles: candleWindow,
481
+ normalizedVolumes,
482
+ confirmedPivots: confirmedPivotState.indices,
483
+ lookbackLeft: PIVOT_LOOKBACK_LEFT,
484
+ lookbackRight: PIVOT_LOOKBACK_RIGHT,
485
+ rangeLower: MIN_BARS_BETWEEN_PIVOTS,
486
+ rangeUpper: MAX_BARS_BETWEEN_PIVOTS
487
+ });
488
+ if (divergence) {
489
+ const modeConfig2 = getModeConfigByKind({
490
+ kind: divergence.kind,
491
+ bullish: BULLISH,
492
+ bearish: BEARISH
493
+ });
494
+ if (!modeConfig2.enable) {
495
+ return strategyApi.skip("STRATEGY_DISABLED");
496
+ }
497
+ const nextPendingCandidate = buildPendingDivergenceCandidate({
498
+ divergence,
499
+ candleWindow,
500
+ direction: modeConfig2.direction,
501
+ pivotLookbackLeft: PIVOT_LOOKBACK_LEFT,
502
+ pivotLookbackRight: PIVOT_LOOKBACK_RIGHT,
503
+ detectedAtTimestamp: timestamp
504
+ });
505
+ const detectionSetupFeatures = buildVolumeDivergenceSetupFeatures({
506
+ candles: candleWindow,
507
+ currentCandle: candle,
508
+ direction: modeConfig2.direction,
509
+ currentPrice: Number(candle.close),
510
+ currentPivotLow: nextPendingCandidate.currentPivotLow,
511
+ previousPivotLow: nextPendingCandidate.previousPivotLow,
512
+ currentPivotHigh: nextPendingCandidate.currentPivotHigh,
513
+ previousPivotHigh: nextPendingCandidate.previousPivotHigh,
514
+ atrPeriod: config2.ATR
515
+ });
516
+ if (detectionSetupFeatures.divergenceAmplitudeAtrRatio != null && detectionSetupFeatures.divergenceAmplitudeAtrRatio < entryThresholds.minDivergenceAmplitudeAtrRatio) {
517
+ return strategyApi.skip("WEAK_DIVERGENCE_AMPLITUDE_ATR");
518
+ }
519
+ pendingCandidate = nextPendingCandidate;
520
+ return strategyApi.skip("WAIT_REVERSAL_CONFIRMATION");
521
+ }
522
+ if (!pendingCandidate) {
523
+ return strategyApi.skip("NO_DIVERGENCE");
524
+ }
525
+ updatePendingCandidateProgress(pendingCandidate, timestamp);
526
+ if (pendingCandidate.barsSinceDetection > maxPendingConfirmationBars) {
527
+ pendingCandidate = null;
528
+ return strategyApi.skip("PENDING_DIVERGENCE_EXPIRED");
529
+ }
530
+ const { currentPrice } = await strategyApi.getMarketData();
531
+ const entryTiming = resolvePendingEntryTiming({
532
+ candidate: pendingCandidate,
533
+ currentPrice
534
+ });
535
+ if (!entryTiming) {
536
+ return strategyApi.skip("WAIT_REVERSAL_CONFIRMATION");
537
+ }
538
+ if (entryTiming === "structure_advance" && !entryThresholds.allowStructureAdvanceEntry) {
539
+ return strategyApi.skip("WAIT_CONFIRMATION_READY");
540
+ }
541
+ const modeConfig = getModeConfigByKind({
542
+ kind: pendingCandidate.kind,
543
+ bullish: BULLISH,
544
+ bearish: BEARISH
545
+ });
546
+ const setupFeatures = buildVolumeDivergenceSetupFeatures({
547
+ candles: candleWindow,
548
+ currentCandle: candle,
549
+ direction: modeConfig.direction,
550
+ currentPrice,
551
+ currentPivotLow: pendingCandidate.currentPivotLow,
552
+ previousPivotLow: pendingCandidate.previousPivotLow,
553
+ currentPivotHigh: pendingCandidate.currentPivotHigh,
554
+ previousPivotHigh: pendingCandidate.previousPivotHigh,
555
+ atrPeriod: config2.ATR
556
+ });
557
+ if (setupFeatures.reclaimPct != null && setupFeatures.reclaimPct < entryThresholds.minReclaimPct) {
558
+ return strategyApi.skip("WAIT_CONFIRMATION_RECLAIM");
559
+ }
560
+ if (setupFeatures.confirmationCandleQuality != null && setupFeatures.confirmationCandleQuality < entryThresholds.minConfirmationCandleQuality) {
561
+ return strategyApi.skip("WAIT_CONFIRMATION_CANDLE_QUALITY");
562
+ }
563
+ const { stopLossPrice, takeProfitPrice, riskRatio, qty } = strategyApi.getDirectionalTpSlPrices({
564
+ price: currentPrice,
565
+ direction: modeConfig.direction,
566
+ takeProfitDelta: modeConfig.TP,
567
+ stopLossDelta: modeConfig.SL,
568
+ unit: "percent",
569
+ maxLossValue: MAX_LOSS_VALUE,
570
+ feePercent: Number(FEE_PERCENT ?? 0)
571
+ });
572
+ if (!qty || !Number.isFinite(qty) || qty <= 0) {
573
+ return strategyApi.skip("INVALID_QTY");
574
+ }
575
+ if (riskRatio <= modeConfig.minRiskRatio) {
576
+ return strategyApi.skip(`RISK_RATIO:${round(riskRatio)}`);
577
+ }
578
+ const indicators = indicatorsState.snapshot();
579
+ const entryPayload = buildEntryPayloadFromPendingCandidate({
580
+ candidate: pendingCandidate,
581
+ candleWindow,
582
+ entryTiming,
583
+ setupFeatures,
584
+ entryThresholds
585
+ });
586
+ lastTradeController.markTrade(timestamp);
587
+ pendingCandidate = null;
588
+ return strategyApi.entry({
589
+ code: "VOLUME_DIVERGENCE_REVERSAL_SIGNAL",
590
+ direction: modeConfig.direction,
591
+ figures: entryPayload.figures,
592
+ indicators,
593
+ additionalIndicators: entryPayload.additionalIndicators,
594
+ orderPlan: {
595
+ qty,
596
+ stopLossPrice,
597
+ takeProfits: [{ rate: 1, price: takeProfitPrice }]
598
+ }
599
+ });
600
+ };
601
+ };
602
+
603
+ // src/VolumeDivergence/strategy.ts
604
+ var VolumeDivergenceStrategyCreator = createStrategyRuntime({
605
+ strategyName: "VolumeDivergence",
606
+ defaults: config,
607
+ createCore: createVolumeDivergenceCore,
608
+ manifest: volumeDivergenceManifest,
609
+ strategyDirectory: __dirname
610
+ });
611
+ export {
612
+ VolumeDivergenceStrategyCreator
613
+ };