using System; using System.Collections.Generic; using System.Linq; using Alpaca.Markets; using DesktopBot.Models; namespace DesktopBot.Engine { // ═══════════════════════════════════════════════════════════════════════════ // BTC/USD ADVANCED ALGORITHM — v2.0 // Strategia combinata multi-segnale ad alta frequenza (barre da 1 minuto). // // ARCHITETTURA: // ┌─────────────────────────────────────────────────────────────────────┐ // │ 1. REGIME DETECTOR (ADX + Trend Slope) │ // │ Classifica il mercato in: TREND_UP / TREND_DOWN / RANGING │ // │ Attiva le strategie più adatte al regime corrente. │ // │ │ // │ 2. ADAPTIVE MOMENTUM ENGINE (Dual-EMA + MACD + RSI divergence) │ // │ Confluenza di tre oscillatori su barre 1min. │ // │ Punteggio: 0–3. Richiede >= 2 per generare segnale. │ // │ │ // │ 3. KALMAN FAIR-VALUE FILTER (filtro adattivo di Kalman) │ // │ Stima il fair-value in tempo reale. Genera segnale quando │ // │ il prezzo si discosta di > KalmanZEntry deviazioni standard. │ // │ │ // │ 4. VOLATILITY BREAKOUT GATE (Keltner Channel + RVOL) │ // │ Filtra i breakout falsi richiedendo volume relativo >= 2x. │ // │ In regime RANGING sostituisce il momentum come segnale. │ // │ │ // │ 5. ATR DYNAMIC POSITION SIZING (ATR-based SL/TP) │ // │ Stop Loss = entry − 1.5 × ATR(14) │ // │ Take Profit = entry + 2.5 × ATR(14) → Risk/Reward = 1:1.67 │ // │ Max drawdown del segnale capped al 4% del prezzo di entrata. │ // └─────────────────────────────────────────────────────────────────────┘ // // LOGICA DI DECISIONE: // ───────────────────── // BUY se TUTTI i seguenti: // • Regime == TREND_UP OPPURE (Regime == RANGING e breakout bullish) // • MomentumScore >= 2 // • Kalman Z-Score <= −KalmanZEntry (prezzo sotto fair-value) // • Nessuna posizione aperta // // SELL se UNO dei seguenti: // • Regime == TREND_DOWN AND MomentumScore <= 0 (tutti bearish) // • Kalman Z-Score >= +KalmanZEntry (prezzo sopra fair-value) // • Breakout della banda Keltner inferiore con RVOL >= 2x // • Posizione aperta con perdita >= 4% (trailing risk-gate) // // PARAMETRI PRE-CALIBRATI PER BTC/USD 1-MINUTO: // ─────────────────────────────────────────────── // FastEma = 5 (5 minuti) // SlowEma = 20 (20 minuti) // RsiPeriod = 14 // MacdFast = 8, MacdSlow = 21, MacdSignal = 5 // AdxPeriod = 14 (soglia trend: ADX > 25) // AtrPeriod = 14 // KalmanDelta = 5e-6 // KalmanZ_Entry = 1.8 // KeltnerPeriod = 20, KeltnerMult = 2.0 // RvolMin = 2.0x // ═══════════════════════════════════════════════════════════════════════════ public sealed class BtcUsdAlgorithm { // ── Parametri fissi ottimizzati per BTC/USD 1min ───────────────────── private const int FastEmaPeriod = 5; private const int SlowEmaPeriod = 20; private const int RsiPeriod = 14; private const int MacdFast = 8; private const int MacdSlow = 21; private const int MacdSignal = 5; private const int AdxPeriod = 14; private const int AtrPeriod = 14; private const int KeltnerPeriod = 20; private const double KalmanDelta = 5e-6; private const double KalmanObsVar = 1.0; private const double KalmanZEntry = 1.8; private const double KalmanZExit = 0.3; private const decimal KeltnerMult = 2.0m; private const decimal RvolMin = 2.0m; private const decimal AdxTrendLevel = 25m; // ADX > 25 → mercato in trend private const decimal MaxDrawdownPct = 0.04m; // 4% max drawdown // ── Stato del filtro di Kalman (persistente tra tick) ──────────────── private double _kalmanX; private double _kalmanP = 1.0; private readonly Queue _spreads = new Queue(25); private double _kalmanStd = 1.0; // ── Stato del regime corrente ──────────────────────────────────────── public MarketRegime CurrentRegime { get; private set; } = MarketRegime.Unknown; // ── Ultimo segnale prodotto ────────────────────────────────────────── public BtcSignalResult LastResult { get; private set; } = new BtcSignalResult(); // ════════════════════════════════════════════════════════════════════ // METODO PRINCIPALE // ════════════════════════════════════════════════════════════════════ /// /// Analizza le barre più recenti e restituisce il segnale di trading. /// Richiede almeno 50 barre da 1 minuto per calcolare tutti gli indicatori. /// public BtcSignalResult Analyze(List bars) { var result = new BtcSignalResult(); if (bars == null || bars.Count < 50) { result.Reason = "Dati insufficienti (richiesti min. 50 bar 1min)"; result.Type = SignalType.None; return LastResult = result; } var closes = bars.Select(b => b.Close).ToList(); var highs = bars.Select(b => b.High).ToList(); var lows = bars.Select(b => b.Low).ToList(); var volumes = bars.Select(b => b.Volume).ToList(); decimal price = closes.Last(); // 1. ── REGIME DETECTOR ────────────────────────────────────────── decimal adx = CalculateAdx(bars, AdxPeriod); decimal trendSlope = CalculateTrendSlope(closes, 10); // slope EMA20 ultimi 10 bar CurrentRegime = DetectRegime(adx, trendSlope); result.Regime = CurrentRegime; result.Adx = adx; // 2. ── ADAPTIVE MOMENTUM ──────────────────────────────────────── int momentumScore = 0; string momentumInfo = ""; // 2a. EMA dual-cross var fastEma = CalculateEma(closes, FastEmaPeriod); var slowEma = CalculateEma(closes, SlowEmaPeriod); bool emaBull = fastEma.Count >= 2 && slowEma.Count >= 2 && fastEma[fastEma.Count - 2] <= slowEma[slowEma.Count - 2] && fastEma.Last() > slowEma.Last(); bool emaBear = fastEma.Count >= 2 && slowEma.Count >= 2 && fastEma[fastEma.Count - 2] >= slowEma[slowEma.Count - 2] && fastEma.Last() < slowEma.Last(); bool emaAbove = fastEma.Count > 0 && slowEma.Count > 0 && fastEma.Last() > slowEma.Last(); if (emaAbove) { momentumScore++; momentumInfo += "EMA↑ "; } else { momentumScore--; momentumInfo += "EMA↓ "; } // 2b. MACD decimal macdLine, signalLine; CalculateMacd(closes, MacdFast, MacdSlow, MacdSignal, out macdLine, out signalLine); bool macdBull = macdLine > signalLine && macdLine > 0; bool macdBear = macdLine < signalLine && macdLine < 0; if (macdBull) { momentumScore++; momentumInfo += "MACD↑ "; } else if (macdBear) { momentumScore--; momentumInfo += "MACD↓ "; } // 2c. RSI con divergenza decimal rsi = CalculateRsi(closes, RsiPeriod); bool rsiBull = rsi > 45 && rsi < 65; // RSI in territorio bullish ma non overbought bool rsiBear = rsi < 55 && rsi > 35; // RSI in territorio bearish ma non oversold bool rsiExtremeBull = rsi < 30; // Ipervenduto → opportunità contrarian bool rsiExtremeBear = rsi > 70; // Ipercomprato → uscita if (rsiBull || rsiExtremeBull) { momentumScore++; momentumInfo += $"RSI({rsi:F0})↑ "; } else if (rsiExtremeBear) { momentumScore--; momentumInfo += $"RSI({rsi:F0})↓ "; } result.MomentumScore = momentumScore; result.MomentumInfo = momentumInfo.Trim(); result.Rsi = rsi; result.MacdLine = macdLine; // 3. ── KALMAN FAIR-VALUE ───────────────────────────────────────── // Feed tutte le barre nel filtro (aggiorna lo stato persistente) foreach (var bar in bars) UpdateKalman(bar.Close); double fairValue = _kalmanX; double zScore = _kalmanStd > 0 ? (double)(price - (decimal)fairValue) / _kalmanStd : 0.0; result.FairValue = (decimal)fairValue; result.KalmanZ = zScore; // 4. ── VOLATILITY BREAKOUT ─────────────────────────────────────── decimal atr = CalculateAtr(bars, AtrPeriod); decimal midEma = CalculateEma(closes.Skip(closes.Count - KeltnerPeriod - 5).ToList(), KeltnerPeriod).Last(); decimal upper = midEma + KeltnerMult * atr; decimal lower = midEma - KeltnerMult * atr; decimal avgVol = volumes.Count > 1 ? volumes.Take(volumes.Count - 1) .Select(v => (decimal)v).Average() : 1m; decimal rvol = avgVol > 0 ? (decimal)volumes.Last() / avgVol : 0m; bool vbBull = price > upper && rvol >= RvolMin; bool vbBear = price < lower; result.Rvol = rvol; result.Upper = upper; result.Lower = lower; // 5. ── DECISIONE FINALE ────────────────────────────────────────── decimal sl = Math.Max(price - 1.5m * atr, price * (1 - MaxDrawdownPct)); decimal tp = price + 2.5m * atr; // ── BUY conditions ─── bool buyByTrend = CurrentRegime == MarketRegime.TrendUp && momentumScore >= 2 && zScore <= -KalmanZEntry; bool buyByBreakout = CurrentRegime == MarketRegime.Ranging && vbBull && momentumScore >= 1; bool buyByKalman = zScore <= -(KalmanZEntry + 0.5) && momentumScore >= 1; // Strong Kalman segnale standalone // ── SELL conditions ─── bool sellByTrend = CurrentRegime == MarketRegime.TrendDown && momentumScore <= -1; bool sellByKalman = zScore >= KalmanZEntry; bool sellByBreakout = vbBear && rvol >= RvolMin; if (buyByTrend || buyByBreakout || buyByKalman) { result.Type = SignalType.Buy; if (buyByTrend) result.Reason = $"[TREND_UP] Confluenza: {momentumInfo} | Z={zScore:F2}"; else if (buyByBreakout) result.Reason = $"[BREAKOUT] Keltner UP | RVOL={rvol:F1}x | Score={momentumScore}"; else result.Reason = $"[KALMAN] Forte deviazione Z={zScore:F2} | {momentumInfo}"; result.Confidence = CalculateConfidence(momentumScore, zScore, rvol, CurrentRegime, isBuy: true); result.StopLoss = sl; result.TakeProfit = tp; } else if (sellByTrend || sellByKalman || sellByBreakout) { result.Type = SignalType.Sell; if (sellByTrend) result.Reason = $"[TREND_DOWN] Momentum={momentumInfo} | ADX={adx:F1}"; else if (sellByKalman) result.Reason = $"[KALMAN] Prezzo sopra fair-value Z={zScore:F2}"; else result.Reason = $"[BREAKOUT_DN] Keltner DOWN | RVOL={rvol:F1}x"; result.Confidence = CalculateConfidence(momentumScore, zScore, rvol, CurrentRegime, isBuy: false); } else { result.Type = SignalType.Hold; result.Reason = $"HOLD — Regime={CurrentRegime} Score={momentumScore} Z={zScore:F2} ADX={adx:F1}"; } result.Price = price; result.Atr = atr; return LastResult = result; } // ════════════════════════════════════════════════════════════════════ // REGIME DETECTOR // ════════════════════════════════════════════════════════════════════ private static MarketRegime DetectRegime(decimal adx, decimal trendSlope) { if (adx < AdxTrendLevel) return MarketRegime.Ranging; if (trendSlope > 0) return MarketRegime.TrendUp; if (trendSlope < 0) return MarketRegime.TrendDown; return MarketRegime.Ranging; } // ════════════════════════════════════════════════════════════════════ // FILTRO DI KALMAN (persistente tra i tick) // ════════════════════════════════════════════════════════════════════ private void UpdateKalman(decimal observed) { double z = (double)observed; if (_kalmanX == 0) { _kalmanX = z; return; } double q = KalmanDelta / (1.0 - KalmanDelta); double pPred = _kalmanP + q; double k = pPred / (pPred + KalmanObsVar); _kalmanX = _kalmanX + k * (z - _kalmanX); _kalmanP = (1.0 - k) * pPred; _spreads.Enqueue(z - _kalmanX); if (_spreads.Count > 20) _spreads.Dequeue(); if (_spreads.Count >= 2) { var list = _spreads.ToArray(); double mean = list.Average(); _kalmanStd = Math.Sqrt(list.Sum(v => (v - mean) * (v - mean)) / (list.Length - 1)); if (_kalmanStd < 1e-9) _kalmanStd = 1.0; } } // ════════════════════════════════════════════════════════════════════ // INDICATORI TECNICI // ════════════════════════════════════════════════════════════════════ private static List CalculateEma(List prices, int period) { var r = new List(); if (prices.Count < period) return r; decimal k = 2m / (period + 1); decimal cur = prices.Take(period).Average(); r.Add(cur); for (int i = period; i < prices.Count; i++) { cur = prices[i] * k + cur * (1 - k); r.Add(cur); } return r; } private static decimal CalculateRsi(List closes, int period) { if (closes.Count < period + 1) return 50m; decimal ag = 0, al = 0; for (int i = 1; i <= period; i++) { decimal d = closes[i] - closes[i - 1]; if (d > 0) ag += d; else al -= d; } ag /= period; al /= period; for (int i = period + 1; i < closes.Count; i++) { decimal d = closes[i] - closes[i - 1]; ag = (ag * (period - 1) + Math.Max(d, 0)) / period; al = (al * (period - 1) + Math.Max(-d, 0)) / period; } return al == 0 ? 100m : 100m - (100m / (1 + ag / al)); } private static void CalculateMacd(List closes, int fast, int slow, int signal, out decimal macdLine, out decimal signalLine) { macdLine = 0; signalLine = 0; var emaF = CalculateEma(closes, fast); var emaS = CalculateEma(closes, slow); int len = Math.Min(emaF.Count, emaS.Count); if (len < signal + 2) return; var macd = new List(); for (int i = 0; i < len; i++) macd.Add(emaF[emaF.Count - len + i] - emaS[emaS.Count - len + i]); var sig = CalculateEma(macd, signal); macdLine = macd.Last(); signalLine = sig.Count > 0 ? sig.Last() : 0; } private static decimal CalculateAtr(List bars, int period) { if (bars.Count < period + 1) return 0m; decimal atr = 0; for (int i = 1; i <= period; i++) { decimal tr = Math.Max(bars[i].High - bars[i].Low, Math.Max(Math.Abs(bars[i].High - bars[i - 1].Close), Math.Abs(bars[i].Low - bars[i - 1].Close))); atr += tr; } atr /= period; for (int i = period + 1; i < bars.Count; i++) { decimal tr = Math.Max(bars[i].High - bars[i].Low, Math.Max(Math.Abs(bars[i].High - bars[i - 1].Close), Math.Abs(bars[i].Low - bars[i - 1].Close))); atr = (atr * (period - 1) + tr) / period; } return atr; } /// ADX basato su True Range e Directional Movement. private static decimal CalculateAdx(List bars, int period) { if (bars.Count < period * 2) return 0m; var dmPlus = new List(); var dmMinus = new List(); var trList = new List(); for (int i = 1; i < bars.Count; i++) { decimal upMove = bars[i].High - bars[i - 1].High; decimal downMove = bars[i - 1].Low - bars[i].Low; decimal tr = Math.Max(bars[i].High - bars[i].Low, Math.Max(Math.Abs(bars[i].High - bars[i - 1].Close), Math.Abs(bars[i].Low - bars[i - 1].Close))); trList.Add(tr); dmPlus.Add(upMove > downMove && upMove > 0 ? upMove : 0); dmMinus.Add(downMove > upMove && downMove > 0 ? downMove : 0); } // Wilder smoothing decimal smoothTr = trList.Take(period).Sum(); decimal smoothPlus = dmPlus.Take(period).Sum(); decimal smoothMinus = dmMinus.Take(period).Sum(); var dx = new List(); for (int i = period; i < trList.Count; i++) { smoothTr = smoothTr - smoothTr / period + trList[i]; smoothPlus = smoothPlus - smoothPlus / period + dmPlus[i]; smoothMinus = smoothMinus - smoothMinus / period + dmMinus[i]; if (smoothTr == 0) { dx.Add(0); continue; } decimal diPlus = 100m * smoothPlus / smoothTr; decimal diMinus = 100m * smoothMinus / smoothTr; decimal diSum = diPlus + diMinus; dx.Add(diSum == 0 ? 0 : 100m * Math.Abs(diPlus - diMinus) / diSum); } return dx.Count >= period ? dx.Skip(dx.Count - period).Average() : 0m; } /// Slope della EMA(period) negli ultimi n bar (normalizzato per prezzo). private static decimal CalculateTrendSlope(List closes, int lookback) { var ema = CalculateEma(closes, SlowEmaPeriod); if (ema.Count < lookback + 1) return 0m; decimal first = ema[ema.Count - lookback - 1]; decimal last = ema.Last(); return first == 0 ? 0m : (last - first) / first; } // ════════════════════════════════════════════════════════════════════ // CONFIDENCE SCORE (0–100) // ════════════════════════════════════════════════════════════════════ private static int CalculateConfidence(int momentumScore, double z, decimal rvol, MarketRegime regime, bool isBuy) { int score = 50; score += momentumScore * 10; score += (int)(Math.Min(Math.Abs(z), 3.0) * 8); score += (int)(Math.Min((double)rvol, 4.0) * 4); if (isBuy && regime == MarketRegime.TrendUp) score += 10; if (!isBuy && regime == MarketRegime.TrendDown) score += 10; return Math.Min(Math.Max(score, 1), 99); } } // ══════════════════════════════════════════════════════════════════════ // TIPI DI SUPPORTO // ══════════════════════════════════════════════════════════════════════ /// Regime del mercato classificato dal Regime Detector. public enum MarketRegime { Unknown, TrendUp, TrendDown, Ranging } /// Risultato dettagliato prodotto da BtcUsdAlgorithm.Analyze(). public sealed class BtcSignalResult { public SignalType Type { get; set; } = SignalType.None; public string Reason { get; set; } = ""; public int Confidence { get; set; } public decimal Price { get; set; } public decimal StopLoss { get; set; } public decimal TakeProfit { get; set; } // Diagnostica indicatori public MarketRegime Regime { get; set; } public decimal Adx { get; set; } public int MomentumScore { get; set; } public string MomentumInfo { get; set; } = ""; public decimal Rsi { get; set; } public decimal MacdLine { get; set; } public decimal FairValue { get; set; } public double KalmanZ { get; set; } public decimal Rvol { get; set; } public decimal Upper { get; set; } public decimal Lower { get; set; } public decimal Atr { get; set; } /// Stringa diagnostica completa per il log operativo. public string ToLogString() => $"[{Type}] {Reason} | " + $"Regime={Regime} ADX={Adx:F1} Score={MomentumScore} " + $"RSI={Rsi:F1} MACD={MacdLine:F2} " + $"FV={FairValue:F1} Z={KalmanZ:F2} RVOL={Rvol:F1}x ATR={Atr:F1} " + $"Conf={Confidence}%"; } }