460 lines
25 KiB
C#
460 lines
25 KiB
C#
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<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 (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
|
||
// ══════════════════════════════════════════════════════════════════════
|
||
|
||
/// <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}%";
|
||
}
|
||
}
|