using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Alpaca.Markets; using DesktopBot.Models; using DesktopBot.Services; namespace DesktopBot.Engine { /// /// Motore di trading automatizzato multi-strategia. /// Strategie supportate: /// 1. EMA Crossover - Golden/Death cross su EMA veloce vs lenta /// 2. RSI - Ipervenduto/Ipercomprato con Wilder RSI /// 3. MACD - Crossover MACD line / Signal line /// 4. Volatility Breakout - Keltner Channel + filtro RVOL (Cap.3 report) /// 5. Kalman Mean Reversion- Fair-value con filtro di Kalman (Cap.4 report) /// public class AutomatedBotEngine { private readonly ITradingService _tradingService; /// Lock per serializzare le operazioni di apertura/chiusura ordini. private readonly SemaphoreSlim _orderExecutionLock = new SemaphoreSlim(1, 1); /// Asset class per il controllo orario mercato (settato dal viewmodel). public string AssetClass { get; set; } = "us_equity"; // ?? eventi esposti al viewmodel ??????????????????????????????????????? public event EventHandler LogGenerated; public event EventHandler SignalGenerated; public event EventHandler EquityUpdated; public event EventHandler WaitingForMarketChanged; // ?? stato runtime ????????????????????????????????????????????????????? private CancellationTokenSource _cts; private Task _loopTask; private bool _isRunning; private BotConfiguration _config; private KalmanFilterState _kalman; private BtcUsdAlgorithm _btcAlgo; private PositionRiskManager _riskManager; public bool IsRunning => _isRunning; public AutomatedBotEngine(ITradingService tradingService) { _tradingService = tradingService ?? throw new ArgumentNullException(nameof(tradingService)); } // ?? avvio / stop ?????????????????????????????????????????????????????? public Task StartAsync(BotConfiguration config) { if (_isRunning) return Task.CompletedTask; _config = config ?? throw new ArgumentNullException(nameof(config)); _kalman = new KalmanFilterState(_config.KalmanDelta, _config.KalmanObservationVariance); _btcAlgo = new BtcUsdAlgorithm(); _riskManager = new PositionRiskManager(_tradingService, _config, Info, Warn); _cts = new CancellationTokenSource(); _isRunning = true; _loopTask = Task.Run(() => BotMainLoopAsync(_cts.Token)); Info($"Bot avviato � strategia: {_config.Strategy}"); return Task.CompletedTask; } public void Stop() { if (!_isRunning) return; _cts?.Cancel(); _isRunning = false; Info("Bot fermato."); } // ?? loop principale ??????????????????????????????????????????????????? private async Task BotMainLoopAsync(CancellationToken ct) { bool waitingState = false; bool authErrorDetected = false; // Flag per tracciare errori di autorizzazione while (!ct.IsCancellationRequested) { try { bool marketOpen = MarketHoursService.IsMarketOpen(AssetClass); if (!marketOpen) { if (!waitingState) { waitingState = true; WaitingForMarketChanged?.Invoke(this, true); } await Task.Delay(TimeSpan.FromSeconds(30), ct); continue; } if (waitingState) { waitingState = false; WaitingForMarketChanged?.Invoke(this, false); } await AnalyzeMarketAsync(ct); authErrorDetected = false; // Reset il flag se una chiamata ha successo } catch (OperationCanceledException) { break; } catch (Exception ex) when (ex.Message.Contains("401") || ex.Message.Contains("Unauthorized") || ex.Message.Contains("non autorizzato")) { if (!authErrorDetected) { Warn("⚠ Errore di autorizzazione rilevato. Il bot si fermerà automaticamente. Verifica API Key e Secret."); authErrorDetected = true; _isRunning = false; break; // Ferma il loop } } catch (Exception ex) { Warn($"Errore nel loop: {ex.Message}"); } await Task.Delay(TimeSpan.FromSeconds(_config.CheckIntervalSeconds), ct); } } // ?? dispatch analisi ?????????????????????????????????????????????????? private async Task AnalyzeMarketAsync(CancellationToken ct) { // Determina il timeframe e il numero di barre da richiedere var timeFrame = _config.AnalysisTimeFrame; int barCount; if (_config.HistoricalBarCount > 0) { // Usa il valore configurato se specificato barCount = _config.HistoricalBarCount; } else { // Calcolo automatico in base al timeframe e alla strategia barCount = timeFrame == BarTimeFrame.Minute ? Math.Max(200, _config.SlowEmaPeriod * 10) // Per 1min: almeno 200 barre : Math.Max(60, _config.SlowEmaPeriod * 3); // Per day/hour: almeno 60 barre } List bars = await FetchBarsWithRetryAsync(_config.Symbol, timeFrame, barCount, ct); if (bars == null) return; // retry esauriti, ciclo saltato try { decimal eq = await _tradingService.GetAvailableEquityAsync(); EquityUpdated?.Invoke(this, eq); } catch { /* non bloccante */ } TradingSignal signal; // BTC/USD: usa l'algoritmo avanzato dedicato if (_config.Symbol.StartsWith("BTC", StringComparison.OrdinalIgnoreCase)) { signal = AnalyzeBtcUsd(bars); } else { switch (_config.Strategy) { case TradingStrategy.EMA_CROSSOVER: signal = AnalyzeEmaCrossover(bars); break; case TradingStrategy.RSI: signal = AnalyzeRsi(bars); break; case TradingStrategy.MACD: signal = AnalyzeMacd(bars); break; case TradingStrategy.VOLATILITY_BREAKOUT: signal = AnalyzeVolatilityBreakout(bars); break; case TradingStrategy.KALMAN_MEAN_REVERSION: signal = AnalyzeKalmanMeanReversion(bars); break; default: return; } } // ── Gestione proattiva delle posizioni aperte (profit lock / stop loss dinamico) ── await _riskManager.ManageOpenPositionsAsync(); if (signal == null || signal.Type == SignalType.None || signal.Type == SignalType.Hold) return; SignalGenerated?.Invoke(this, signal); Info($"SIGNAL {signal.Type}: {signal.Reason} (confidenza: {signal.Confidence}%)"); // Acquisisci il lock per serializzare le operazioni di ordine await _orderExecutionLock.WaitAsync(); try { bool hasPos = await _tradingService.HasOpenPositionAsync(_config.Symbol); if (signal.Type == SignalType.Buy && !hasPos) { // Verifica risk prima di aprire bool canOpen = await _riskManager.CanOpenNewPositionAsync(); if (canOpen) await ExecuteOrderAsync(signal); } if (signal.Type == SignalType.Sell && hasPos) { Info($"[SIGNAL] Segnale SELL: chiusura posizione {_config.Symbol}."); await _tradingService.ClosePositionAsync(_config.Symbol); } } finally { _orderExecutionLock.Release(); } } // -- Fetch con retry lineare ----------------------------------------- private async System.Threading.Tasks.Task> FetchBarsWithRetryAsync( string symbol, Alpaca.Markets.BarTimeFrame timeFrame, int barCount, System.Threading.CancellationToken ct) { const int MaxRetries = 10; const int RetryStepSec = 10; for (int attempt = 1; attempt <= MaxRetries; attempt++) { if (ct.IsCancellationRequested) return null; var bars = await _tradingService.GetHistoricalBarsAsync(symbol, timeFrame, barCount); if (bars != null && bars.Count >= 30) return bars; int waitSec = attempt * RetryStepSec; Warn(string.Format("Dati storici insufficienti ({0} barre). Tentativo {1}/{2} tra {3}s...", bars?.Count ?? 0, attempt, MaxRetries, waitSec)); try { await System.Threading.Tasks.Task.Delay(System.TimeSpan.FromSeconds(waitSec), ct); } catch (System.OperationCanceledException) { return null; } } Warn("Dati storici non disponibili dopo tutti i tentativi. Ciclo saltato."); return null; } // ?? ALGORITMO BTC/USD (dispatcher verso BtcUsdAlgorithm) ?????????????? private TradingSignal AnalyzeBtcUsd(List bars) { var result = _btcAlgo.Analyze(bars); Info($"[BTC] {result.ToLogString()}"); if (result.Type == SignalType.Buy) return MakeSignal(SignalType.Buy, result.Price, result.Reason, result.Confidence, result.StopLoss, result.TakeProfit); if (result.Type == SignalType.Sell) return MakeSignal(SignalType.Sell, result.Price, result.Reason, result.Confidence); return Neutral(); } // ?? STRATEGIA 1: EMA CROSSOVER ????????????????????????????????????????? // BUY : EMA veloce incrocia al rialzo EMA lenta (golden cross) // SELL: EMA veloce incrocia al ribasso EMA lenta (death cross) private TradingSignal AnalyzeEmaCrossover(List bars) { var closes = bars.Select(b => b.Close).ToList(); var fastEma = CalculateEma(closes, _config.FastEmaPeriod); var slowEma = CalculateEma(closes, _config.SlowEmaPeriod); if (fastEma.Count < 2 || slowEma.Count < 2) return Neutral(); decimal fPrev = fastEma[fastEma.Count - 2], sPrev = slowEma[slowEma.Count - 2]; decimal fCurr = fastEma[fastEma.Count - 1], sCurr = slowEma[slowEma.Count - 1]; decimal price = closes.Last(), atr = CalculateAtr(bars, 14); if (fPrev <= sPrev && fCurr > sCurr) return MakeSignal(SignalType.Buy, price, $"Golden cross EMA{_config.FastEmaPeriod}/EMA{_config.SlowEmaPeriod}", 70, atr > 0 ? price - atr * 1.5m : price * (1 - _config.StopLossPercentage), atr > 0 ? price + atr * 2.5m : price * (1 + _config.TakeProfitPercentage)); if (fPrev >= sPrev && fCurr < sCurr) return MakeSignal(SignalType.Sell, price, $"Death cross EMA{_config.FastEmaPeriod}/EMA{_config.SlowEmaPeriod}", 65); return Neutral(); } // ?? STRATEGIA 2: RSI ?????????????????????????????????????????????????? // BUY : RSI < soglia oversold // SELL: RSI > soglia overbought private TradingSignal AnalyzeRsi(List bars) { var closes = bars.Select(b => b.Close).ToList(); decimal rsi = CalculateRsi(closes, _config.RsiPeriod); decimal price = closes.Last(), atr = CalculateAtr(bars, 14); if (rsi <= _config.RsiOversoldThreshold) return MakeSignal(SignalType.Buy, price, $"RSI oversold ({rsi:F1})", 65, atr > 0 ? price - atr * 1.5m : price * (1 - _config.StopLossPercentage), atr > 0 ? price + atr * 2.0m : price * (1 + _config.TakeProfitPercentage)); if (rsi >= _config.RsiOverboughtThreshold) return MakeSignal(SignalType.Sell, price, $"RSI overbought ({rsi:F1})", 65); return Neutral(); } // ?? STRATEGIA 3: MACD ????????????????????????????????????????????????? // BUY : MACD line incrocia al rialzo la signal line // SELL: MACD line incrocia al ribasso la signal line private TradingSignal AnalyzeMacd(List bars) { var closes = bars.Select(b => b.Close).ToList(); var emaFast = CalculateEma(closes, _config.MacdFastPeriod); var emaSlow = CalculateEma(closes, _config.MacdSlowPeriod); int minLen = Math.Min(emaFast.Count, emaSlow.Count); if (minLen < _config.MacdSignalPeriod + 2) return Neutral(); var macdLine = new List(); for (int i = 0; i < minLen; i++) macdLine.Add(emaFast[emaFast.Count - minLen + i] - emaSlow[emaSlow.Count - minLen + i]); var sigLine = CalculateEma(macdLine, _config.MacdSignalPeriod); if (sigLine.Count < 2) return Neutral(); decimal mPrev = macdLine[macdLine.Count - 2], mCurr = macdLine[macdLine.Count - 1]; decimal sPrev = sigLine[sigLine.Count - 2], sCurr = sigLine[sigLine.Count - 1]; decimal price = closes.Last(), atr = CalculateAtr(bars, 14); if (mPrev <= sPrev && mCurr > sCurr) return MakeSignal(SignalType.Buy, price, "MACD crossover bullish", 72, atr > 0 ? price - atr * 1.5m : price * (1 - _config.StopLossPercentage), atr > 0 ? price + atr * 2.5m : price * (1 + _config.TakeProfitPercentage)); if (mPrev >= sPrev && mCurr < sCurr) return MakeSignal(SignalType.Sell, price, "MACD crossover bearish", 68); return Neutral(); } // ?? STRATEGIA 4: VOLATILITY BREAKOUT (Keltner + RVOL) ???????????????? // Capitolo 3 del report: breakout banda Keltner + conferma volume relativo // BUY : close > upper band AND RVOL >= RvolMinThreshold // SELL: close < lower band // SL = entry - AtrStopMultiplier*ATR | TP = entry + 2*AtrStopMultiplier*ATR private TradingSignal AnalyzeVolatilityBreakout(List bars) { int period = _config.KeltnerPeriod; if (bars.Count < period + 5) return Neutral(); var recent = bars.Skip(bars.Count - period - 5).ToList(); var closes = recent.Select(b => b.Close).ToList(); var vols = recent.Select(b => b.Volume).ToList(); decimal atr = CalculateAtr(recent, period); decimal midEma = CalculateEma(closes, period).Last(); decimal upper = midEma + _config.KeltnerMultiplier * atr; decimal lower = midEma - _config.KeltnerMultiplier * atr; decimal price = closes.Last(); decimal avgVol = vols.Take(vols.Count - 1).Average(); decimal rvol = avgVol > 0 ? vols.Last() / (decimal)avgVol : 0m; Info($"[VBKOUT] Close={price:F2} Upper={upper:F2} Lower={lower:F2} RVOL={rvol:F2}"); if (price > upper && rvol >= _config.RvolMinThreshold) { decimal d = _config.AtrStopMultiplier * atr; return MakeSignal(SignalType.Buy, price, $"Keltner breakout UP - RVOL={rvol:F2}x", 78, price - d, price + d * 2m); } if (price < lower) return MakeSignal(SignalType.Sell, price, "Keltner breakout DOWN", 70); return Neutral(); } // ?? STRATEGIA 5: KALMAN MEAN REVERSION ??????????????????????????????? // Capitolo 4 del report: filtro di Kalman per stima del fair value. // BUY : Z-Score <= -entryThreshold (prezzo molto sotto fair value) // SELL: Z-Score >= +entryThreshold (prezzo molto sopra fair value) // EXIT: |Z-Score| <= exitThreshold (convergenza alla media) private TradingSignal AnalyzeKalmanMeanReversion(List bars) { if (bars.Count < 30) return Neutral(); foreach (var bar in bars) _kalman.Update(bar.Close); decimal price = bars.Last().Close, fair = (decimal)_kalman.StateEstimate; double z = _kalman.SpreadStdDev > 0 ? (double)(price - fair) / _kalman.SpreadStdDev : 0.0; Info($"[KALMAN] Price={price:F2} Fair={fair:F2} Z={z:F3}"); decimal atr = CalculateAtr(bars, 14); if (z <= -_config.KalmanEntryZScore) return MakeSignal(SignalType.Buy, price, $"Kalman BUY (z={z:F2})", Math.Min(95, (int)(Math.Abs(z) * 30)), atr > 0 ? price - atr * 2m : price * (1 - _config.StopLossPercentage), fair); if (z >= _config.KalmanEntryZScore) return MakeSignal(SignalType.Sell, price, $"Kalman SELL (z={z:F2})", Math.Min(95, (int)(Math.Abs(z) * 30))); if (Math.Abs(z) <= _config.KalmanExitZScore) return MakeSignal(SignalType.Sell, price, $"Kalman EXIT convergenza (z={z:F2})", 80); return Neutral(); } // ?? esecuzione ordine bracket ????????????????????????????????????????? private async Task ExecuteOrderAsync(TradingSignal signal) { try { // ── Verifica FINALE prima di piazzare ── // Riconta le posizioni in tempo reale da Alpaca IReadOnlyList positions = null; try { positions = await _tradingService.GetBotPositionsAsync(); } catch (Exception exPos) { Warn($"[VERIFY] Errore nel recupero posizioni: {exPos.Message}. Procedo comunque con caution."); // Continua lo stesso, ma con più cautela } int openCount = positions?.Count ?? 0; bool hasPosition = positions?.Any(p => string.Equals(p.Symbol, signal.Symbol, StringComparison.OrdinalIgnoreCase)) ?? false; if (signal.Type == SignalType.Buy && hasPosition) { Warn($"[VERIFY] Posizione già aperta su {signal.Symbol}; ordine non piazzato."); return; } // Verifica che non abbia superato il limite globale if (signal.Type == SignalType.Buy && openCount >= _config.MaxOpenPositions) { Warn($"[VERIFY] Limite posizioni raggiunto ({openCount}/{_config.MaxOpenPositions}); ordine non piazzato."); return; } // Verifica per-asset int assetCount = positions?.Count(p => string.Equals( p.Symbol, signal.Symbol, StringComparison.OrdinalIgnoreCase)) ?? 0; if (signal.Type == SignalType.Buy && assetCount >= _config.MaxOpenPositionsPerAsset) { Warn($"[VERIFY] Limite posizioni per asset raggiunto ({assetCount}/{_config.MaxOpenPositionsPerAsset}); ordine non piazzato."); return; } decimal equity = 0; try { equity = await _tradingService.GetAvailableEquityAsync(); } catch (Exception exEq) { Warn($"[VERIFY] Errore nel recupero equity: {exEq.Message}. Procedo con cautela."); } bool isCryptoOrder2 = !string.IsNullOrEmpty(AssetClass) && AssetClass.IndexOf("crypto", StringComparison.OrdinalIgnoreCase) >= 0; decimal qty; if (signal.Price > 0) { qty = await _riskManager.ComputeQuantityAsync(signal.Price, isCryptoOrder2); } else { qty = _config.Quantity; } if (qty <= 0) { Warn($"[VERIFY] Capitale insufficiente (equity: ${equity:F2}) o limite risk raggiunto. Ordine non piazzato."); return; } // Log stato prima di piazzare Info($"[PRE-ORDER] Posizioni aperte: {openCount}/{_config.MaxOpenPositions} | Equity: ${equity:F2} | Qty da piazzare: {qty}"); bool isCryptoOrder = isCryptoOrder2; decimal sl = signal.StopLoss > 0 ? signal.StopLoss : signal.Price * (1 - _config.StopLossPercentage); decimal tp = signal.TakeProfit > 0 ? signal.TakeProfit : signal.Price * (1 + _config.TakeProfitPercentage); sl = Math.Max(sl, signal.Price * 0.80m); tp = Math.Min(tp, signal.Price * 1.50m); if (isCryptoOrder) { // Alpaca crypto non supporta bracket/OTOCO: piazza market + stop/limit separati var entry = await _tradingService.PlaceMarketOrderAsync(signal.Symbol, qty, signal.Side); Info($"✓ Market {signal.Side} {qty}x {signal.Symbol} @ ~${signal.Price:F2} | #{entry?.OrderId}"); // Stop-loss separato (lato opposto) var stopSide = signal.Side == OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy; try { // arrotonda stop price a 2 decimali per passare la validazione Alpaca decimal slRounded = Math.Round(sl, 2); decimal tpRounded = Math.Round(tp, 2); // Garantisce che SL < prezzo corrente per Buy (stop sell) if (signal.Side == OrderSide.Buy && slRounded >= signal.Price) slRounded = Math.Round(signal.Price * 0.99m, 2); if (signal.Side == OrderSide.Sell && slRounded <= signal.Price) slRounded = Math.Round(signal.Price * 1.01m, 2); var slOrder = await _tradingService.PlaceStopOrderAsync(signal.Symbol, qty, slRounded, stopSide); Info($"✓ Stop-loss {stopSide} @ ${slRounded:F2} | #{slOrder?.OrderId}"); var tpOrder = await _tradingService.PlaceLimitOrderAsync(signal.Symbol, qty, tpRounded, stopSide); Info($"✓ Take-profit {stopSide} @ ${tpRounded:F2} | #{tpOrder?.OrderId}"); } catch (Exception exProt) { Warn($"SL/TP separati non piazzati: {exProt.Message}"); } } else { var order = await _tradingService.PlaceBracketOrderAsync( signal.Symbol, qty, signal.Price, tp, sl, signal.Side); Info($"✓ Bracket {signal.Side} {qty}x {signal.Symbol} @ ${signal.Price:F2} | SL=${sl:F2} TP=${tp:F2} | #{order?.OrderId}"); } } catch (Exception ex) { if (ex.Message.Contains("401") || ex.Message.Contains("Unauthorized") || ex.Message.Contains("non autorizzato")) Warn($"⚠ Errore di autorizzazione Alpaca: {ex.Message}. Verifica API Key e Secret."); else Warn($"Errore esecuzione ordine: {ex.Message}"); } } // ?? indicatori tecnici ???????????????????????????????????????????????? private static List CalculateEma(List v, int period) { var r = new List(); if (v.Count < period) return r; decimal k = 2m / (period + 1), cur = v.Take(period).Average(); r.Add(cur); for (int i = period; i < v.Count; i++) { cur = v[i] * k + cur * (1 - k); r.Add(cur); } return r; } private static decimal CalculateRsi(List c, int period) { if (c.Count < period + 1) return 50m; decimal ag = 0, al = 0; for (int i = 1; i <= period; i++) { decimal d = c[i] - c[i - 1]; if (d > 0) ag += d; else al -= d; } ag /= period; al /= period; for (int i = period + 1; i < c.Count; i++) { decimal d = c[i] - c[i - 1], g = d > 0 ? d : 0, l = d < 0 ? -d : 0; ag = (ag * (period - 1) + g) / period; al = (al * (period - 1) + l) / period; } if (al == 0) return 100m; return 100m - (100m / (1 + ag / al)); } 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 hl = bars[i].High - bars[i].Low; decimal hpc = Math.Abs(bars[i].High - bars[i - 1].Close); decimal lpc = Math.Abs(bars[i].Low - bars[i - 1].Close); atr += Math.Max(hl, Math.Max(hpc, lpc)); } atr /= period; for (int i = period + 1; i < bars.Count; i++) { decimal hl = bars[i].High - bars[i].Low; decimal hpc = Math.Abs(bars[i].High - bars[i - 1].Close); decimal lpc = Math.Abs(bars[i].Low - bars[i - 1].Close); decimal tr = Math.Max(hl, Math.Max(hpc, lpc)); atr = (atr * (period - 1) + tr) / period; } return atr; } // ????????????????????????????????????????????????????????????????????? private TradingSignal MakeSignal(SignalType t, decimal price, string reason, int conf, decimal sl = 0, decimal tp = 0) => new TradingSignal { Type = t, Symbol = _config.Symbol, Price = price, Timestamp = DateTime.UtcNow, Reason = reason, Confidence = conf, StopLoss = sl, TakeProfit = tp }; private static TradingSignal Neutral() => new TradingSignal { Type = SignalType.None }; private void Info(string msg) => LogGenerated?.Invoke(this, new BotLogEntry(LogLevel.Info, msg)); private void Warn(string msg) => LogGenerated?.Invoke(this, new BotLogEntry(LogLevel.Warning, msg)); } // ??????????????????????????????????????????????????????????????????????????? // FILTRO DI KALMAN � stato persistente single-asset // Capitolo 4 del report: stima del fair value a guadagno variabile. // Predict: P_pred = P + Q (Q = delta/(1-delta)) // Update : K = P_pred/(P_pred+R) // x = x + K*(z-x) P = (1-K)*P_pred // ??????????????????????????????????????????????????????????????????????????? internal sealed class KalmanFilterState { private readonly double _delta, _r; private double _x, _p; private readonly Queue _spreads = new Queue(); private const int SpreadWindow = 20; public double StateEstimate => _x; public double SpreadStdDev { get; private set; } public KalmanFilterState(double delta, double observationVariance) { _delta = delta; _r = observationVariance; _x = 0; _p = 1; } public void Update(decimal observedPrice) { double z = (double)observedPrice; if (_x == 0) { _x = z; _p = 1; return; } double q = _delta / (1 - _delta), pPred = _p + q, k = pPred / (pPred + _r); _x = _x + k * (z - _x); _p = (1 - k) * pPred; _spreads.Enqueue(z - _x); if (_spreads.Count > SpreadWindow) _spreads.Dequeue(); SpreadStdDev = Std(_spreads); } private static double Std(IEnumerable v) { var l = v.ToList(); if (l.Count < 2) return 1.0; double m = l.Average(); return Math.Sqrt(l.Sum(x => (x - m) * (x - m)) / (l.Count - 1)); } } }