prab-cli 1.2.6 → 1.2.7

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.
@@ -2,10 +2,13 @@
2
2
  /**
3
3
  * Technical Analysis Module
4
4
  * Calculates EMA and generates trading signals
5
+ * Primary indicators: EMA 10 and EMA 20 with tilt detection
5
6
  */
6
7
  Object.defineProperty(exports, "__esModule", { value: true });
7
8
  exports.calculateEMA = calculateEMA;
8
9
  exports.calculateAllEMAs = calculateAllEMAs;
10
+ exports.calculateEMATilt = calculateEMATilt;
11
+ exports.checkMarketConditions = checkMarketConditions;
9
12
  exports.calculateIndicators = calculateIndicators;
10
13
  exports.generateSignal = generateSignal;
11
14
  exports.formatSignalSummary = formatSignalSummary;
@@ -32,19 +35,180 @@ function calculateEMA(prices, period) {
32
35
  return ema;
33
36
  }
34
37
  /**
35
- * Calculate all EMA values
38
+ * Calculate all EMA values (primary: 10 and 20)
36
39
  */
37
40
  function calculateAllEMAs(candles) {
38
41
  const closePrices = candles.map((c) => c.close);
39
42
  return {
40
- ema9: calculateEMA(closePrices, 9),
41
- ema21: calculateEMA(closePrices, 21),
43
+ ema10: calculateEMA(closePrices, 10),
44
+ ema20: calculateEMA(closePrices, 20),
42
45
  ema50: calculateEMA(closePrices, 50),
43
46
  ema200: calculateEMA(closePrices, 200),
44
47
  };
45
48
  }
46
49
  /**
47
- * Detect EMA crossover patterns
50
+ * Calculate EMA tilt/slope
51
+ * Looks at recent EMA values to determine direction and strength
52
+ * @param emaValues - Array of EMA values
53
+ * @param lookback - Number of periods to analyze (default 3)
54
+ * @returns Tilt information including direction, angle, and strength
55
+ */
56
+ function calculateEMATilt(emaValues, lookback = 3) {
57
+ if (emaValues.length < lookback + 1) {
58
+ return { direction: "flat", angle: 0, strength: "LOW", slopePercent: 0 };
59
+ }
60
+ const current = emaValues[emaValues.length - 1];
61
+ const previous = emaValues[emaValues.length - 1 - lookback];
62
+ // Calculate percentage change (slope)
63
+ const slopePercent = ((current - previous) / previous) * 100;
64
+ // Normalize to angle-like value (-100 to 100)
65
+ // Multiply by factor to make small changes more visible
66
+ const angle = Math.max(-100, Math.min(100, slopePercent * 50));
67
+ // Determine direction
68
+ let direction;
69
+ if (slopePercent > 0.05) {
70
+ direction = "bullish";
71
+ }
72
+ else if (slopePercent < -0.05) {
73
+ direction = "bearish";
74
+ }
75
+ else {
76
+ direction = "flat";
77
+ }
78
+ // Determine strength based on slope magnitude
79
+ let strength;
80
+ const absSlope = Math.abs(slopePercent);
81
+ if (absSlope > 0.3) {
82
+ strength = "HIGH";
83
+ }
84
+ else if (absSlope > 0.1) {
85
+ strength = "MEDIUM";
86
+ }
87
+ else {
88
+ strength = "LOW";
89
+ }
90
+ return { direction, angle, strength, slopePercent };
91
+ }
92
+ /**
93
+ * Check market conditions for trade validity
94
+ * Detects sideways markets, EMA intersections, and validates EMA angle
95
+ * Based on visual analysis: good trades have clear EMA separation and optimal angle
96
+ *
97
+ * @param ema10Values - Array of EMA10 values
98
+ * @param ema20Values - Array of EMA20 values
99
+ * @param ema10Tilt - EMA10 tilt information
100
+ * @param ema20Tilt - EMA20 tilt information
101
+ * @param currentPrice - Current price
102
+ * @returns Market condition check result
103
+ */
104
+ function checkMarketConditions(ema10Values, ema20Values, ema10Tilt, ema20Tilt, currentPrice) {
105
+ const currentEMA10 = ema10Values[ema10Values.length - 1];
106
+ const currentEMA20 = ema20Values[ema20Values.length - 1];
107
+ // Calculate EMA separation percentage
108
+ const emaSeparationPercent = Math.abs((currentEMA10 - currentEMA20) / currentEMA20) * 100;
109
+ // Check for EMA intersection/convergence (EMAs too close together)
110
+ // If separation is less than 0.15%, EMAs are effectively intersecting
111
+ const emasIntersecting = emaSeparationPercent < 0.15;
112
+ // Check for sideways market: EMAs are flat or diverging in different directions
113
+ // Sideways when: both tilts are LOW strength, or tilts are in opposite directions
114
+ const bothTiltsFlat = ema10Tilt.strength === "LOW" && ema20Tilt.strength === "LOW";
115
+ const tiltsConflicting = ema10Tilt.direction !== "flat" &&
116
+ ema20Tilt.direction !== "flat" &&
117
+ ema10Tilt.direction !== ema20Tilt.direction;
118
+ // Check for recent crossovers (looking back 5 candles) - indicates choppy market
119
+ let recentCrossoverCount = 0;
120
+ const lookback = Math.min(5, ema10Values.length - 1, ema20Values.length - 1);
121
+ for (let i = 1; i <= lookback; i++) {
122
+ const prevIdx = ema10Values.length - 1 - i;
123
+ const currIdx = ema10Values.length - i;
124
+ if (prevIdx >= 0 && currIdx >= 0) {
125
+ const prevAbove = ema10Values[prevIdx] > ema20Values[prevIdx];
126
+ const currAbove = ema10Values[currIdx] > ema20Values[currIdx];
127
+ if (prevAbove !== currAbove) {
128
+ recentCrossoverCount++;
129
+ }
130
+ }
131
+ }
132
+ // Multiple crossovers indicate choppy/sideways market
133
+ const multipleCrossovers = recentCrossoverCount >= 2;
134
+ // Check if EMA10 angle is in optimal range for trading
135
+ // - Not too flat (abs slope < 0.05%) = no trend
136
+ // - Not too steep (abs slope > 1.5%) = extreme/overextended
137
+ // Optimal range: 0.1% to 0.8% slope (good momentum without being extreme)
138
+ const absSlope = Math.abs(ema10Tilt.slopePercent);
139
+ const tooFlat = absSlope < 0.08;
140
+ const tooSteep = absSlope > 1.2;
141
+ const emaAngleInOptimalRange = !tooFlat && !tooSteep;
142
+ // Determine if market is sideways
143
+ const isSideways = bothTiltsFlat ||
144
+ tiltsConflicting ||
145
+ multipleCrossovers ||
146
+ (emasIntersecting && !emaAngleInOptimalRange);
147
+ // Final decision: should we trade?
148
+ let shouldTrade = true;
149
+ let reason = "Market conditions favorable for trading";
150
+ if (emasIntersecting) {
151
+ shouldTrade = false;
152
+ reason = `EMAs intersecting (separation: ${emaSeparationPercent.toFixed(3)}%) - wait for clear trend`;
153
+ }
154
+ else if (isSideways) {
155
+ shouldTrade = false;
156
+ if (multipleCrossovers) {
157
+ reason = "Choppy market - multiple EMA crossovers detected, wait for trend to establish";
158
+ }
159
+ else if (tiltsConflicting) {
160
+ reason = "EMAs diverging in opposite directions - conflicting signals, wait for alignment";
161
+ }
162
+ else {
163
+ reason = "Sideways market - EMAs showing no clear direction, wait for breakout";
164
+ }
165
+ }
166
+ else if (tooFlat) {
167
+ shouldTrade = false;
168
+ reason = `EMA10 angle too flat (${ema10Tilt.slopePercent.toFixed(3)}%) - no momentum, wait for trend`;
169
+ }
170
+ else if (tooSteep) {
171
+ shouldTrade = false;
172
+ reason = `EMA10 angle too steep (${ema10Tilt.slopePercent.toFixed(3)}%) - overextended, wait for pullback`;
173
+ }
174
+ return {
175
+ isSideways,
176
+ emasIntersecting,
177
+ emaSeparationPercent,
178
+ emaAngleInOptimalRange,
179
+ shouldTrade,
180
+ reason,
181
+ };
182
+ }
183
+ /**
184
+ * Check if EMA 10 is tilting towards the candle (high probability setup)
185
+ * @param currentPrice - Current candle close price
186
+ * @param ema10 - Current EMA 10 value
187
+ * @param ema10Tilt - EMA 10 tilt information
188
+ * @param ema20 - Current EMA 20 value
189
+ * @returns true if high probability setup detected
190
+ */
191
+ function isHighProbabilitySetup(currentPrice, ema10, ema10Tilt, ema20) {
192
+ const priceAboveEMA10 = currentPrice > ema10;
193
+ const priceAboveEMA20 = currentPrice > ema20;
194
+ const ema10AboveEMA20 = ema10 > ema20;
195
+ // High probability BUY: Price above EMAs, EMA10 > EMA20, EMA10 tilting up
196
+ if (priceAboveEMA10 && ema10AboveEMA20 && ema10Tilt.direction === "bullish") {
197
+ return true;
198
+ }
199
+ // High probability SELL: Price below EMAs, EMA10 < EMA20, EMA10 tilting down
200
+ if (!priceAboveEMA10 && !ema10AboveEMA20 && ema10Tilt.direction === "bearish") {
201
+ return true;
202
+ }
203
+ // Reversal setup: Price touches EMA10 from above/below with strong tilt
204
+ const distanceToEMA10 = Math.abs((currentPrice - ema10) / ema10) * 100;
205
+ if (distanceToEMA10 < 0.5 && ema10Tilt.strength === "HIGH") {
206
+ return true;
207
+ }
208
+ return false;
209
+ }
210
+ /**
211
+ * Detect EMA crossover patterns (EMA10 vs EMA20)
48
212
  */
49
213
  function detectCrossover(shortEMA, longEMA, lookback = 3) {
50
214
  if (shortEMA.length < lookback + 1 || longEMA.length < lookback + 1) {
@@ -54,11 +218,11 @@ function detectCrossover(shortEMA, longEMA, lookback = 3) {
54
218
  const currentLong = longEMA[longEMA.length - 1];
55
219
  const prevShort = shortEMA[shortEMA.length - lookback];
56
220
  const prevLong = longEMA[longEMA.length - lookback];
57
- // Golden cross: short EMA crosses above long EMA
221
+ // Golden cross: EMA10 crosses above EMA20
58
222
  if (prevShort <= prevLong && currentShort > currentLong) {
59
223
  return "golden";
60
224
  }
61
- // Death cross: short EMA crosses below long EMA
225
+ // Death cross: EMA10 crosses below EMA20
62
226
  if (prevShort >= prevLong && currentShort < currentLong) {
63
227
  return "death";
64
228
  }
@@ -67,8 +231,8 @@ function detectCrossover(shortEMA, longEMA, lookback = 3) {
67
231
  /**
68
232
  * Determine price position relative to EMAs
69
233
  */
70
- function getPriceVsEMA(currentPrice, ema9, ema21, ema50) {
71
- const aboveCount = [ema9, ema21, ema50].filter((ema) => currentPrice > ema).length;
234
+ function getPriceVsEMA(currentPrice, ema10, ema20, ema50) {
235
+ const aboveCount = [ema10, ema20, ema50].filter((ema) => currentPrice > ema).length;
72
236
  if (aboveCount === 3)
73
237
  return "above_all";
74
238
  if (aboveCount === 0)
@@ -76,68 +240,125 @@ function getPriceVsEMA(currentPrice, ema9, ema21, ema50) {
76
240
  return "mixed";
77
241
  }
78
242
  /**
79
- * Calculate trend strength based on EMA alignment
243
+ * Calculate trend strength based on EMA alignment and tilt
80
244
  */
81
- function calculateTrendStrength(currentPrice, ema9, ema21, ema50, ema200) {
245
+ function calculateTrendStrength(currentPrice, ema10, ema20, ema50, ema200, ema10Tilt) {
82
246
  let strength = 0;
83
- // Check EMA alignment (bullish: 9 > 21 > 50 > 200)
84
- if (ema9 > ema21)
85
- strength += 20;
86
- if (ema21 > ema50)
87
- strength += 20;
247
+ // Check EMA alignment (bullish: 10 > 20 > 50 > 200)
248
+ if (ema10 > ema20)
249
+ strength += 15;
250
+ if (ema20 > ema50)
251
+ strength += 15;
88
252
  if (ema200 !== undefined && ema50 > ema200)
89
- strength += 20;
253
+ strength += 15;
90
254
  // Check price vs EMAs
91
- if (currentPrice > ema9)
255
+ if (currentPrice > ema10)
92
256
  strength += 10;
93
- if (currentPrice > ema21)
257
+ if (currentPrice > ema20)
94
258
  strength += 10;
95
259
  if (currentPrice > ema50)
96
260
  strength += 10;
97
261
  if (ema200 !== undefined && currentPrice > ema200)
98
262
  strength += 10;
263
+ // Tilt adds significant strength
264
+ if (ema10Tilt.strength === "HIGH")
265
+ strength += 15;
266
+ else if (ema10Tilt.strength === "MEDIUM")
267
+ strength += 10;
268
+ else
269
+ strength += 5;
99
270
  return Math.min(strength, 100);
100
271
  }
272
+ /**
273
+ * Determine overall probability level
274
+ */
275
+ function determineProbability(ema10Tilt, ema20Tilt, crossover, priceVsEMA) {
276
+ let score = 0;
277
+ // Strong tilt in same direction = high probability
278
+ if (ema10Tilt.strength === "HIGH")
279
+ score += 3;
280
+ else if (ema10Tilt.strength === "MEDIUM")
281
+ score += 2;
282
+ else
283
+ score += 1;
284
+ if (ema20Tilt.strength === "HIGH")
285
+ score += 2;
286
+ else if (ema20Tilt.strength === "MEDIUM")
287
+ score += 1;
288
+ // Tilts aligned = higher probability
289
+ if (ema10Tilt.direction === ema20Tilt.direction && ema10Tilt.direction !== "flat") {
290
+ score += 2;
291
+ }
292
+ // Recent crossover = higher probability
293
+ if (crossover !== "none")
294
+ score += 2;
295
+ // Clear price position = higher probability
296
+ if (priceVsEMA !== "mixed")
297
+ score += 1;
298
+ if (score >= 7)
299
+ return "HIGH";
300
+ if (score >= 4)
301
+ return "MEDIUM";
302
+ return "LOW";
303
+ }
101
304
  /**
102
305
  * Calculate technical indicators
103
306
  */
104
307
  function calculateIndicators(data) {
105
308
  const ema = calculateAllEMAs(data.candles);
106
- const currentEMA9 = ema.ema9[ema.ema9.length - 1] || 0;
107
- const currentEMA21 = ema.ema21[ema.ema21.length - 1] || 0;
309
+ const currentEMA10 = ema.ema10[ema.ema10.length - 1] || 0;
310
+ const currentEMA20 = ema.ema20[ema.ema20.length - 1] || 0;
108
311
  const currentEMA50 = ema.ema50[ema.ema50.length - 1] || 0;
109
312
  const currentEMA200 = ema.ema200.length > 0 ? ema.ema200[ema.ema200.length - 1] : undefined;
110
- // Detect crossover between EMA9 and EMA21
111
- const emaCrossover = detectCrossover(ema.ema9, ema.ema21);
313
+ // Calculate EMA tilts (key indicator for probability)
314
+ const ema10Tilt = calculateEMATilt(ema.ema10);
315
+ const ema20Tilt = calculateEMATilt(ema.ema20);
316
+ // Detect crossover between EMA10 and EMA20
317
+ const emaCrossover = detectCrossover(ema.ema10, ema.ema20);
112
318
  // Price vs EMA position
113
- const priceVsEMA = getPriceVsEMA(data.currentPrice, currentEMA9, currentEMA21, currentEMA50);
114
- // Determine trend
319
+ const priceVsEMA = getPriceVsEMA(data.currentPrice, currentEMA10, currentEMA20, currentEMA50);
320
+ // Determine trend based on EMA alignment
115
321
  let trend;
116
- if (currentEMA9 > currentEMA21 && currentEMA21 > currentEMA50) {
322
+ if (currentEMA10 > currentEMA20 && currentEMA20 > currentEMA50) {
117
323
  trend = "bullish";
118
324
  }
119
- else if (currentEMA9 < currentEMA21 && currentEMA21 < currentEMA50) {
325
+ else if (currentEMA10 < currentEMA20 && currentEMA20 < currentEMA50) {
120
326
  trend = "bearish";
121
327
  }
122
328
  else {
123
329
  trend = "neutral";
124
330
  }
125
- // Calculate trend strength
126
- const trendStrength = calculateTrendStrength(data.currentPrice, currentEMA9, currentEMA21, currentEMA50, currentEMA200);
331
+ // Calculate trend strength (includes tilt)
332
+ const trendStrength = calculateTrendStrength(data.currentPrice, currentEMA10, currentEMA20, currentEMA50, currentEMA200, ema10Tilt);
333
+ // Determine probability level
334
+ const probability = determineProbability(ema10Tilt, ema20Tilt, emaCrossover, priceVsEMA);
335
+ // Check for high probability alert trigger
336
+ const alertTrigger = isHighProbabilitySetup(data.currentPrice, currentEMA10, ema10Tilt, currentEMA20);
337
+ // Check market conditions (sideways, intersecting EMAs, angle validation)
338
+ const marketCondition = checkMarketConditions(ema.ema10, ema.ema20, ema10Tilt, ema20Tilt, data.currentPrice);
127
339
  return {
128
340
  ema,
129
- currentEMA9,
130
- currentEMA21,
341
+ currentEMA10,
342
+ currentEMA20,
131
343
  currentEMA50,
132
344
  currentEMA200: currentEMA200 || 0,
345
+ // Legacy aliases for compatibility
346
+ currentEMA9: currentEMA10,
347
+ currentEMA21: currentEMA20,
348
+ ema10Tilt,
349
+ ema20Tilt,
133
350
  emaCrossover,
134
351
  priceVsEMA,
135
352
  trend,
136
353
  trendStrength,
354
+ probability,
355
+ alertTrigger,
356
+ marketCondition,
137
357
  };
138
358
  }
139
359
  /**
140
- * Generate trading signal based on EMA analysis
360
+ * Generate trading signal based on EMA 10/20 analysis with tilt detection
361
+ * Now includes market condition filtering to avoid sideways/choppy markets
141
362
  */
142
363
  function generateSignal(data) {
143
364
  const indicators = calculateIndicators(data);
@@ -146,99 +367,162 @@ function generateSignal(data) {
146
367
  let confidence = 50;
147
368
  let stopLoss = 3;
148
369
  let takeProfit = 6;
149
- // EMA Crossover signals
150
- if (indicators.emaCrossover === "golden") {
151
- signal = "BUY";
152
- confidence += 25;
153
- reasoning.push("Golden cross detected (EMA9 crossed above EMA21)");
154
- }
155
- else if (indicators.emaCrossover === "death") {
156
- signal = "SELL";
157
- confidence += 25;
158
- reasoning.push("Death cross detected (EMA9 crossed below EMA21)");
370
+ const { ema10Tilt, ema20Tilt, alertTrigger, probability, marketCondition } = indicators;
371
+ // === MARKET CONDITION CHECK (Priority Filter) ===
372
+ // Check for sideways market, intersecting EMAs, or suboptimal EMA angle
373
+ if (!marketCondition.shouldTrade) {
374
+ reasoning.push(`⚠️ ${marketCondition.reason}`);
375
+ // Add specific details about the market condition
376
+ if (marketCondition.emasIntersecting) {
377
+ reasoning.push(`EMA separation: ${marketCondition.emaSeparationPercent.toFixed(3)}% (too close)`);
378
+ }
379
+ if (marketCondition.isSideways) {
380
+ reasoning.push("Market showing ranging/sideways behavior");
381
+ }
382
+ if (!marketCondition.emaAngleInOptimalRange) {
383
+ const absSlope = Math.abs(ema10Tilt.slopePercent);
384
+ if (absSlope < 0.08) {
385
+ reasoning.push(`EMA10 slope: ${ema10Tilt.slopePercent.toFixed(3)}% (too flat for reliable signal)`);
386
+ }
387
+ else if (absSlope > 1.2) {
388
+ reasoning.push(`EMA10 slope: ${ema10Tilt.slopePercent.toFixed(3)}% (too steep, may reverse)`);
389
+ }
390
+ }
391
+ // Return HOLD with reduced confidence
392
+ return {
393
+ signal: "HOLD",
394
+ confidence: 25,
395
+ stopLoss: 3,
396
+ takeProfit: 6,
397
+ indicators,
398
+ reasoning,
399
+ };
159
400
  }
160
- // Price position relative to EMAs
161
- if (indicators.priceVsEMA === "above_all") {
162
- if (signal !== "SELL") {
163
- signal = signal === "HOLD" ? "BUY" : signal;
164
- confidence += 15;
401
+ // Add positive market condition note
402
+ reasoning.push(`✓ EMA separation: ${marketCondition.emaSeparationPercent.toFixed(2)}% - clear trend`);
403
+ // === PRIMARY SIGNAL: EMA 10 Tilt Analysis ===
404
+ if (ema10Tilt.direction === "bullish") {
405
+ if (ema10Tilt.strength === "HIGH") {
406
+ signal = "BUY";
407
+ confidence += 30;
408
+ reasoning.push(`EMA10 strongly tilted UP (${ema10Tilt.slopePercent.toFixed(3)}%) - HIGH probability`);
409
+ }
410
+ else if (ema10Tilt.strength === "MEDIUM") {
411
+ signal = "BUY";
412
+ confidence += 20;
413
+ reasoning.push(`EMA10 tilted UP (${ema10Tilt.slopePercent.toFixed(3)}%) - MEDIUM probability`);
414
+ }
415
+ else {
416
+ reasoning.push(`EMA10 slightly tilted UP - weak signal`);
165
417
  }
166
- reasoning.push("Price is above all major EMAs (9, 21, 50)");
167
418
  }
168
- else if (indicators.priceVsEMA === "below_all") {
169
- if (signal !== "BUY") {
170
- signal = signal === "HOLD" ? "SELL" : signal;
171
- confidence += 15;
419
+ else if (ema10Tilt.direction === "bearish") {
420
+ if (ema10Tilt.strength === "HIGH") {
421
+ signal = "SELL";
422
+ confidence += 30;
423
+ reasoning.push(`EMA10 strongly tilted DOWN (${ema10Tilt.slopePercent.toFixed(3)}%) - HIGH probability`);
424
+ }
425
+ else if (ema10Tilt.strength === "MEDIUM") {
426
+ signal = "SELL";
427
+ confidence += 20;
428
+ reasoning.push(`EMA10 tilted DOWN (${ema10Tilt.slopePercent.toFixed(3)}%) - MEDIUM probability`);
429
+ }
430
+ else {
431
+ reasoning.push(`EMA10 slightly tilted DOWN - weak signal`);
172
432
  }
173
- reasoning.push("Price is below all major EMAs (9, 21, 50)");
174
433
  }
175
434
  else {
176
- reasoning.push("Price is mixed relative to EMAs - consolidation phase");
435
+ reasoning.push("EMA10 is flat - no directional bias");
436
+ }
437
+ // === EMA 20 Tilt Confirmation ===
438
+ if (ema20Tilt.direction === ema10Tilt.direction && ema10Tilt.direction !== "flat") {
439
+ confidence += 15;
440
+ reasoning.push(`EMA20 confirms direction (${ema20Tilt.direction}) - trend alignment`);
441
+ }
442
+ else if (ema20Tilt.direction !== ema10Tilt.direction && ema20Tilt.direction !== "flat") {
443
+ confidence -= 10;
444
+ reasoning.push(`Warning: EMA20 diverging from EMA10 - potential reversal`);
445
+ }
446
+ // === EMA Crossover signals ===
447
+ if (indicators.emaCrossover === "golden") {
448
+ if (signal !== "SELL")
449
+ signal = "BUY";
450
+ confidence += 20;
451
+ reasoning.push("Golden cross detected (EMA10 crossed above EMA20)");
177
452
  }
178
- // Trend analysis
179
- if (indicators.trend === "bullish") {
453
+ else if (indicators.emaCrossover === "death") {
454
+ if (signal !== "BUY")
455
+ signal = "SELL";
456
+ confidence += 20;
457
+ reasoning.push("Death cross detected (EMA10 crossed below EMA20)");
458
+ }
459
+ // === Price position relative to EMAs ===
460
+ if (indicators.priceVsEMA === "above_all") {
180
461
  if (signal !== "SELL") {
462
+ signal = signal === "HOLD" ? "BUY" : signal;
181
463
  confidence += 10;
182
464
  }
183
- reasoning.push("EMAs are aligned bullishly (EMA9 > EMA21 > EMA50)");
465
+ reasoning.push("Price above all EMAs (10, 20, 50) - bullish structure");
184
466
  }
185
- else if (indicators.trend === "bearish") {
467
+ else if (indicators.priceVsEMA === "below_all") {
186
468
  if (signal !== "BUY") {
469
+ signal = signal === "HOLD" ? "SELL" : signal;
187
470
  confidence += 10;
188
471
  }
189
- reasoning.push("EMAs are aligned bearishly (EMA9 < EMA21 < EMA50)");
472
+ reasoning.push("Price below all EMAs (10, 20, 50) - bearish structure");
190
473
  }
191
474
  else {
192
- reasoning.push("EMAs show no clear trend alignment");
475
+ reasoning.push("Price mixed relative to EMAs - consolidation");
476
+ }
477
+ // === Alert Trigger (High Probability Setup) ===
478
+ if (alertTrigger) {
479
+ confidence += 15;
480
+ reasoning.push("⚡ HIGH PROBABILITY SETUP: EMA10 tilting towards price action");
193
481
  }
194
- // EMA200 analysis (if available)
482
+ // === EMA200 analysis (long-term context) ===
195
483
  if (indicators.currentEMA200 > 0) {
196
484
  if (data.currentPrice > indicators.currentEMA200) {
197
485
  if (signal === "BUY")
198
- confidence += 10;
199
- reasoning.push("Price is above EMA200 (long-term bullish)");
486
+ confidence += 5;
487
+ reasoning.push("Above EMA200 - long-term bullish context");
200
488
  }
201
489
  else {
202
490
  if (signal === "SELL")
203
- confidence += 10;
204
- reasoning.push("Price is below EMA200 (long-term bearish)");
491
+ confidence += 5;
492
+ reasoning.push("Below EMA200 - long-term bearish context");
205
493
  }
206
494
  }
207
- // 24h price change consideration
208
- if (Math.abs(data.priceChangePercent24h) > 5) {
209
- if (data.priceChangePercent24h > 5 && signal === "BUY") {
210
- reasoning.push(`Strong momentum: +${data.priceChangePercent24h.toFixed(2)}% in 24h`);
211
- confidence += 5;
212
- }
213
- else if (data.priceChangePercent24h < -5 && signal === "SELL") {
214
- reasoning.push(`Strong downward momentum: ${data.priceChangePercent24h.toFixed(2)}% in 24h`);
215
- confidence += 5;
216
- }
217
- else if (Math.abs(data.priceChangePercent24h) > 10) {
218
- reasoning.push(`Caution: High volatility (${data.priceChangePercent24h.toFixed(2)}% in 24h)`);
219
- confidence -= 5;
220
- }
495
+ // === Distance from EMA10 (entry quality) ===
496
+ const distanceFromEMA10 = ((data.currentPrice - indicators.currentEMA10) / indicators.currentEMA10) * 100;
497
+ if (Math.abs(distanceFromEMA10) < 0.3) {
498
+ confidence += 10;
499
+ reasoning.push(`Price near EMA10 (${distanceFromEMA10.toFixed(2)}%) - optimal entry zone`);
500
+ }
501
+ else if (Math.abs(distanceFromEMA10) > 2) {
502
+ confidence -= 5;
503
+ reasoning.push(`Price extended from EMA10 (${distanceFromEMA10.toFixed(2)}%) - wait for pullback`);
221
504
  }
222
- // Adjust stop-loss and take-profit based on signal and confidence
505
+ // === Stop-loss and Take-profit calculation ===
223
506
  if (signal === "BUY") {
224
- // Set stop-loss below recent support (EMA21)
225
- const distanceToEMA21 = ((data.currentPrice - indicators.currentEMA21) / data.currentPrice) * 100;
226
- stopLoss = Math.max(2, Math.min(distanceToEMA21 + 1, 5));
227
- takeProfit = stopLoss * 2; // 2:1 risk-reward ratio
507
+ // Stop-loss below EMA20
508
+ const distanceToEMA20 = ((data.currentPrice - indicators.currentEMA20) / data.currentPrice) * 100;
509
+ stopLoss = Math.max(1.5, Math.min(distanceToEMA20 + 0.5, 4));
510
+ takeProfit = stopLoss * 2.5; // 2.5:1 risk-reward
228
511
  }
229
512
  else if (signal === "SELL") {
230
- // For sell signals, stop-loss above resistance
231
- const distanceToEMA21 = ((indicators.currentEMA21 - data.currentPrice) / data.currentPrice) * 100;
232
- stopLoss = Math.max(2, Math.min(Math.abs(distanceToEMA21) + 1, 5));
233
- takeProfit = stopLoss * 2;
513
+ const distanceToEMA20 = ((indicators.currentEMA20 - data.currentPrice) / data.currentPrice) * 100;
514
+ stopLoss = Math.max(1.5, Math.min(Math.abs(distanceToEMA20) + 0.5, 4));
515
+ takeProfit = stopLoss * 2.5;
234
516
  }
235
- // Cap confidence at 95%
517
+ // === Final confidence adjustment ===
236
518
  confidence = Math.min(confidence, 95);
237
519
  confidence = Math.max(confidence, 10);
238
- // If no clear signals, suggest HOLD
239
- if (reasoning.length <= 2 && indicators.emaCrossover === "none") {
520
+ // If no clear signal with weak tilt, recommend HOLD
521
+ if (signal === "HOLD" || (ema10Tilt.strength === "LOW" && indicators.emaCrossover === "none")) {
240
522
  signal = "HOLD";
241
- reasoning.push("No clear trading signal - recommend waiting for better setup");
523
+ if (!reasoning.some((r) => r.includes("waiting"))) {
524
+ reasoning.push("No strong tilt detected - wait for EMA10 to show direction");
525
+ }
242
526
  }
243
527
  return {
244
528
  signal,
@@ -259,17 +543,37 @@ function formatSignalSummary(signal, symbol, price) {
259
543
  : signal.indicators.trend === "bearish"
260
544
  ? "\u{2198}\u{FE0F}"
261
545
  : "\u{27A1}\u{FE0F}";
546
+ const tiltEmoji = signal.indicators.ema10Tilt.direction === "bullish"
547
+ ? "\u{2191}"
548
+ : signal.indicators.ema10Tilt.direction === "bearish"
549
+ ? "\u{2193}"
550
+ : "\u{2194}";
551
+ const probEmoji = signal.indicators.probability === "HIGH"
552
+ ? "\u{1F525}"
553
+ : signal.indicators.probability === "MEDIUM"
554
+ ? "\u{1F7E1}"
555
+ : "\u{26AA}";
556
+ const marketCondition = signal.indicators.marketCondition;
557
+ const marketStatus = marketCondition?.shouldTrade
558
+ ? "✓ Tradeable"
559
+ : "⚠️ Avoid (Sideways/Intersecting)";
262
560
  return `
263
561
  ${signalEmoji} Signal: ${signal.signal}
264
562
  Confidence: ${signal.confidence}%
563
+ Probability: ${signal.indicators.probability} ${probEmoji}
564
+ Market: ${marketStatus}
265
565
  Stop-Loss: ${signal.stopLoss}%
266
566
  Take-Profit: ${signal.takeProfit}%
267
567
 
268
568
  Current Price: $${price.toLocaleString()}
269
569
  Trend: ${signal.indicators.trend} ${trendEmoji}
270
- EMA9: $${signal.indicators.currentEMA9.toFixed(2)}
271
- EMA21: $${signal.indicators.currentEMA21.toFixed(2)}
570
+
571
+ EMA10: $${signal.indicators.currentEMA10.toFixed(2)} ${tiltEmoji} (tilt: ${signal.indicators.ema10Tilt.slopePercent.toFixed(3)}%)
572
+ EMA20: $${signal.indicators.currentEMA20.toFixed(2)}
573
+ EMA Separation: ${marketCondition?.emaSeparationPercent.toFixed(2) || "N/A"}%
272
574
  EMA50: $${signal.indicators.currentEMA50.toFixed(2)}
273
575
  ${signal.indicators.currentEMA200 > 0 ? `EMA200: $${signal.indicators.currentEMA200.toFixed(2)}` : ""}
576
+ ${signal.indicators.alertTrigger ? "\n⚡ ALERT: High probability setup detected!" : ""}
577
+ ${!marketCondition?.shouldTrade ? `\n⚠️ ${marketCondition?.reason || "Market conditions not favorable"}` : ""}
274
578
  `;
275
579
  }