Sviluppo TradingBot

This commit is contained in:
2026-06-09 18:29:41 +02:00
parent 61f1e59964
commit e3c0bd51b2
133 changed files with 24903 additions and 1 deletions
+459
View File
@@ -0,0 +1,459 @@
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: 03. 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<double> _spreads = new Queue<double>(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
// ════════════════════════════════════════════════════════════════════
/// <summary>
/// Analizza le barre più recenti e restituisce il segnale di trading.
/// Richiede almeno 50 barre da 1 minuto per calcolare tutti gli indicatori.
/// </summary>
public BtcSignalResult Analyze(List<IBar> 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<decimal> CalculateEma(List<decimal> prices, int period)
{
var r = new List<decimal>();
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<decimal> 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<decimal> 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<decimal>();
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<IBar> 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;
}
/// <summary>ADX basato su True Range e Directional Movement.</summary>
private static decimal CalculateAdx(List<IBar> bars, int period)
{
if (bars.Count < period * 2) return 0m;
var dmPlus = new List<decimal>();
var dmMinus = new List<decimal>();
var trList = new List<decimal>();
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<decimal>();
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;
}
/// <summary>Slope della EMA(period) negli ultimi n bar (normalizzato per prezzo).</summary>
private static decimal CalculateTrendSlope(List<decimal> 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 (0100)
// ════════════════════════════════════════════════════════════════════
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
// ══════════════════════════════════════════════════════════════════════
/// <summary>Regime del mercato classificato dal Regime Detector.</summary>
public enum MarketRegime { Unknown, TrendUp, TrendDown, Ranging }
/// <summary>Risultato dettagliato prodotto da BtcUsdAlgorithm.Analyze().</summary>
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; }
/// <summary>Stringa diagnostica completa per il log operativo.</summary>
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}%";
}
}