Files
Encelado/DesktopBot/Engine/BtcUsdAlgorithm.cs
T
2026-06-09 18:29:41 +02:00

460 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}%";
}
}