Sviluppo TradingBot
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
public class AutomatedBotEngine
|
||||
{
|
||||
private readonly ITradingService _tradingService;
|
||||
/// <summary>Lock per serializzare le operazioni di apertura/chiusura ordini.</summary>
|
||||
private readonly SemaphoreSlim _orderExecutionLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>Asset class per il controllo orario mercato (settato dal viewmodel).</summary>
|
||||
public string AssetClass { get; set; } = "us_equity";
|
||||
|
||||
// ?? eventi esposti al viewmodel ???????????????????????????????????????
|
||||
public event EventHandler<BotLogEntry> LogGenerated;
|
||||
public event EventHandler<TradingSignal> SignalGenerated;
|
||||
public event EventHandler<decimal> EquityUpdated;
|
||||
public event EventHandler<bool> 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<IBar> 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<System.Collections.Generic.List<Alpaca.Markets.IBar>> 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<IBar> 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<IBar> 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<IBar> 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<IBar> 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<decimal>();
|
||||
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<IBar> 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<IBar> 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<IPosition> 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<decimal> CalculateEma(List<decimal> v, int period)
|
||||
{
|
||||
var r = new List<decimal>();
|
||||
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<decimal> 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<IBar> 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<double> _spreads = new Queue<double>();
|
||||
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<double> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: 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}%";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Alpaca.Markets;
|
||||
using DesktopBot.Models;
|
||||
using DesktopBot.Services;
|
||||
|
||||
namespace DesktopBot.Engine
|
||||
{
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// POSITION RISK MANAGER
|
||||
// Valuta la situazione del portafoglio live PRIMA di aprire nuove posizioni
|
||||
// e gestisce le uscite in profitto / in perdita in modo proattivo.
|
||||
//
|
||||
// Regole applicate:
|
||||
// 1. MaxOpenPositions → blocca nuove aperture se già raggiunte
|
||||
// 2. MaxCapitalAllocated → blocca se troppo capitale è già impegnato
|
||||
// 3. ProfitLockPercent → chiude posizioni con PnL% >= soglia (lock profit)
|
||||
// 4. MaxLossPercent → chiude posizioni con PnL% <= -soglia (stop loss dinamico)
|
||||
// 5. UseBreakEvenStop → quando PnL% >= ProfitLockPercent/2, annota il
|
||||
// break-even e lo riporta al chiamante per aggiornare
|
||||
// l'ordine stop (o come informazione di log)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
internal sealed class PositionRiskManager
|
||||
{
|
||||
private readonly ITradingService _svc;
|
||||
private readonly BotConfiguration _cfg;
|
||||
private readonly Action<string> _logInfo;
|
||||
private readonly Action<string> _logWarn;
|
||||
|
||||
public PositionRiskManager(
|
||||
ITradingService service,
|
||||
BotConfiguration config,
|
||||
Action<string> logInfo,
|
||||
Action<string> logWarn)
|
||||
{
|
||||
_svc = service;
|
||||
_cfg = config;
|
||||
_logInfo = logInfo;
|
||||
_logWarn = logWarn;
|
||||
}
|
||||
|
||||
// ── Verifica se è possibile aprire un nuovo ordine ──────────────────
|
||||
/// <returns>true = ok aprire, false = bloccato dal risk manager</returns>
|
||||
public async Task<bool> CanOpenNewPositionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Conta solo le posizioni aperte dal bot (ClientOrderId BOT_)
|
||||
var positions = await _svc.GetBotPositionsAsync();
|
||||
int openCount = positions?.Count ?? 0;
|
||||
|
||||
// 1. Limite numero posizioni globale
|
||||
if (openCount >= _cfg.MaxOpenPositions)
|
||||
{
|
||||
_logWarn($"[RISK] Apertura bloccata: {openCount} posizioni aperte su {_cfg.MaxOpenPositions} consentite.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Limite posizioni per asset
|
||||
int assetCount = positions?.Count(p => string.Equals(
|
||||
p.Symbol, _cfg.Symbol, StringComparison.OrdinalIgnoreCase)) ?? 0;
|
||||
if (assetCount >= _cfg.MaxOpenPositionsPerAsset)
|
||||
{
|
||||
_logWarn($"[RISK] Apertura bloccata: {assetCount} posizioni aperte su {_cfg.Symbol} (limite per asset: {_cfg.MaxOpenPositionsPerAsset}).");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Limite capitale allocato
|
||||
if (positions != null && positions.Count > 0)
|
||||
{
|
||||
decimal equity = await _svc.GetAvailableEquityAsync();
|
||||
decimal totalEquity = equity + positions.Sum(p => Math.Abs(p.MarketValue ?? 0));
|
||||
decimal allocated = positions.Sum(p => Math.Abs(p.MarketValue ?? 0));
|
||||
decimal allocPct = totalEquity > 0 ? allocated / totalEquity : 0;
|
||||
|
||||
if (allocPct >= _cfg.MaxCapitalAllocatedPercent)
|
||||
{
|
||||
_logWarn($"[RISK] Apertura bloccata: capitale allocato {allocPct:P1} >= limite {_cfg.MaxCapitalAllocatedPercent:P1}.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logWarn($"[RISK] Errore controllo posizioni: {ex.Message} — apertura permessa per sicurezza.");
|
||||
return true; // fail-open: non bloccare per errori di rete
|
||||
}
|
||||
}
|
||||
|
||||
// ── Calcola la dimensione ottimale per la nuova posizione ────────────
|
||||
/// <summary>
|
||||
/// Ritorna la quantità da comprare in base al capitale disponibile e
|
||||
/// alla quota assegnabile a questa specifica posizione.
|
||||
/// </summary>
|
||||
public async Task<decimal> ComputeQuantityAsync(decimal price, bool isCrypto)
|
||||
{
|
||||
if (price <= 0) return 0;
|
||||
|
||||
decimal equity = await _svc.GetAvailableEquityAsync();
|
||||
|
||||
// Considera solo le posizioni aperte dal bot per stimare il capitale già allocato
|
||||
IReadOnlyList<IPosition> positions;
|
||||
try { positions = await _svc.GetBotPositionsAsync(); }
|
||||
catch { positions = new List<IPosition>(); }
|
||||
|
||||
decimal allocated = positions?.Sum(p => Math.Abs(p.MarketValue ?? 0)) ?? 0m;
|
||||
decimal totalEquity = equity + allocated;
|
||||
|
||||
// Budget massimo assoluto per questa posizione
|
||||
decimal maxBudget = totalEquity * _cfg.MaxPositionSizePercent;
|
||||
|
||||
// Non superare la quota di capitale libero disponibile
|
||||
decimal usableBudget = Math.Min(maxBudget, equity * 0.95m); // riserva 5% liquidità
|
||||
|
||||
decimal rawQty = usableBudget / price;
|
||||
|
||||
decimal qty = isCrypto
|
||||
? Math.Round(rawQty, 4, MidpointRounding.ToEven)
|
||||
: Math.Floor(rawQty);
|
||||
|
||||
if (qty <= 0)
|
||||
_logWarn($"[RISK] Quantità calcolata = 0 (budget usabile: {usableBudget:F2}, prezzo: {price:F2}).");
|
||||
|
||||
return qty;
|
||||
}
|
||||
|
||||
// ── Gestione proattiva delle posizioni aperte (profit lock / stop) ───
|
||||
/// <summary>
|
||||
/// Valuta ogni posizione aperta per il simbolo del bot. Se una posizione
|
||||
/// supera la soglia di profitto o di perdita, la chiude emettendo log adeguati.
|
||||
/// Chiamare a ogni ciclo PRIMA della valutazione del segnale.
|
||||
/// </summary>
|
||||
public async Task ManageOpenPositionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var position = await _svc.GetPositionAsync(_cfg.Symbol);
|
||||
if (position == null) return;
|
||||
|
||||
decimal entryPrice = position.AverageEntryPrice;
|
||||
decimal currentPrice = position.AssetCurrentPrice ?? entryPrice;
|
||||
decimal qty = position.Quantity;
|
||||
decimal marketValue = position.MarketValue ?? (currentPrice * qty);
|
||||
decimal costBasis = position.CostBasis != 0 ? position.CostBasis : entryPrice * qty;
|
||||
|
||||
if (costBasis == 0) return;
|
||||
|
||||
decimal unrealizedPnl = marketValue - costBasis;
|
||||
decimal pnlPct = unrealizedPnl / costBasis;
|
||||
|
||||
_logInfo($"[RISK] Posizione {_cfg.Symbol}: ingresso={entryPrice:F2} corrente={currentPrice:F2} PnL={pnlPct:P2} ({unrealizedPnl:+F2;-F2})");
|
||||
|
||||
// ── Profit Lock: chiudi se in profitto sopra soglia ──────────
|
||||
if (_cfg.ProfitLockPercent > 0 && pnlPct >= _cfg.ProfitLockPercent)
|
||||
{
|
||||
_logInfo($"[RISK] PROFIT LOCK: PnL {pnlPct:P2} >= {_cfg.ProfitLockPercent:P2}. Chiudo posizione.");
|
||||
await _svc.ClosePositionAsync(_cfg.Symbol);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Break-even alert (solo log, SL fisico già piazzato) ──────
|
||||
if (_cfg.UseBreakEvenStop && _cfg.ProfitLockPercent > 0)
|
||||
{
|
||||
decimal breakEvenTrigger = _cfg.ProfitLockPercent / 2m;
|
||||
if (pnlPct >= breakEvenTrigger)
|
||||
_logInfo($"[RISK] Break-even zone raggiunta (PnL {pnlPct:P2}). Lo stop-loss dovrebbe essere al prezzo di ingresso ({entryPrice:F2}).");
|
||||
}
|
||||
|
||||
// ── Stop Loss dinamico: chiudi se in perdita oltre soglia ────
|
||||
if (_cfg.MaxLossPercent > 0 && pnlPct <= -_cfg.MaxLossPercent)
|
||||
{
|
||||
_logWarn($"[RISK] STOP LOSS DINAMICO: PnL {pnlPct:P2} <= -{_cfg.MaxLossPercent:P2}. Chiudo posizione per limitare le perdite.");
|
||||
await _svc.ClosePositionAsync(_cfg.Symbol);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// La posizione potrebbe non esistere (già chiusa automaticamente dall'ordine SL/TP)
|
||||
if (!ex.Message.Contains("position does not exist", StringComparison.OrdinalIgnoreCase) &&
|
||||
!ex.Message.Contains("404", StringComparison.OrdinalIgnoreCase))
|
||||
_logWarn($"[RISK] Errore gestione posizione: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Report sintetico della situazione del portafoglio ────────────────
|
||||
public async Task LogPortfolioStatusAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var positions = await _svc.GetAllPositionsAsync();
|
||||
if (positions == null || positions.Count == 0)
|
||||
{
|
||||
_logInfo("[RISK] Nessuna posizione aperta.");
|
||||
return;
|
||||
}
|
||||
|
||||
decimal totalUnrealized = positions.Sum(p => p.UnrealizedProfitLoss.GetValueOrDefault());
|
||||
_logInfo($"[RISK] Portafoglio: {positions.Count} posizioni aperte | PnL totale non realizzato: {totalUnrealized:+F2;-F2}");
|
||||
|
||||
foreach (var p in positions)
|
||||
{
|
||||
decimal cb = p.CostBasis != 0 ? p.CostBasis : 1m;
|
||||
decimal pnlPct = p.UnrealizedProfitLoss.GetValueOrDefault() / cb;
|
||||
_logInfo($" [{p.Symbol}] qty={p.Quantity} entry={p.AverageEntryPrice:F2} mktVal={p.MarketValue:F2} PnL={pnlPct:P2}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logWarn($"[RISK] Errore lettura portafoglio: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DesktopBot.Models;
|
||||
|
||||
namespace DesktopBot.Engine
|
||||
{
|
||||
/// <summary>
|
||||
/// Motore di raccomandazione strategia.
|
||||
/// Per ogni combinazione asset-class / volatilità / tipologia di mercato restituisce
|
||||
/// la strategia ottimale e i parametri pre-calibrati.
|
||||
///
|
||||
/// Logica ispirata agli approcci professionali:
|
||||
/// Crypto → alta volatilità 24/7 → VOLATILITY_BREAKOUT
|
||||
/// Equity → orario di mercato → EMA_CROSSOVER o MACD
|
||||
/// ETF → bassa volatilità → KALMAN_MEAN_REVERSION
|
||||
/// FX/FX-alike → mean-reverting → RSI
|
||||
/// </summary>
|
||||
public static class StrategyAdvisor
|
||||
{
|
||||
// ── Profili per asset class ─────────────────────────────────────────
|
||||
|
||||
private static readonly Dictionary<string, StrategyProfile[]> _profiles
|
||||
= new Dictionary<string, StrategyProfile[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// ── Crypto ──────────────────────────────────────────────────────
|
||||
["crypto"] = new[]
|
||||
{
|
||||
new StrategyProfile(
|
||||
TradingStrategy.VOLATILITY_BREAKOUT,
|
||||
"Volatility Breakout (1min)",
|
||||
"Strategia ad alta frequenza per crypto: breakout su barre da 1 minuto con filtro RVOL e CVD.",
|
||||
"🚀", "#FF6D00",
|
||||
isRecommended: true,
|
||||
cfg =>
|
||||
{
|
||||
cfg.KeltnerPeriod = 20;
|
||||
cfg.KeltnerMultiplier = 2.0m;
|
||||
cfg.RvolMinThreshold = 2.5m; // Soglia RVOL più alta per crypto ad alta volatilità
|
||||
cfg.AtrStopMultiplier = 1.0m;
|
||||
cfg.CheckIntervalSeconds = 60; // Tick ogni 60 secondi
|
||||
cfg.AnalysisTimeFrame = Alpaca.Markets.BarTimeFrame.Minute; // Barre da 1 minuto
|
||||
cfg.HistoricalBarCount = 200; // 200 barre = 3.3 ore di storico
|
||||
cfg.StopLossPercentage = 0.03m; // 3% SL per volatilità crypto
|
||||
cfg.TakeProfitPercentage = 0.06m; // 6% TP, ratio 1:2
|
||||
}),
|
||||
|
||||
new StrategyProfile(
|
||||
TradingStrategy.KALMAN_MEAN_REVERSION,
|
||||
"Kalman Mean Reversion (1min)",
|
||||
"Mean-reversion adattiva su crypto range-bound con analisi a 1 minuto.",
|
||||
"🔬", "#40C4FF",
|
||||
isRecommended: false,
|
||||
cfg =>
|
||||
{
|
||||
cfg.KalmanDelta = 1e-5;
|
||||
cfg.KalmanObservationVariance = 1.0;
|
||||
cfg.KalmanEntryZScore = 2.0;
|
||||
cfg.KalmanExitZScore = 0.25;
|
||||
cfg.CheckIntervalSeconds = 120;
|
||||
cfg.AnalysisTimeFrame = Alpaca.Markets.BarTimeFrame.Minute;
|
||||
cfg.HistoricalBarCount = 300; // 5 ore di storico
|
||||
cfg.StopLossPercentage = 0.03m;
|
||||
cfg.TakeProfitPercentage = 0.06m;
|
||||
}),
|
||||
|
||||
new StrategyProfile(
|
||||
TradingStrategy.EMA_CROSSOVER,
|
||||
"EMA Crossover (1min)",
|
||||
"Trend-following veloce su barre da 1 minuto — ottimale per BTC/USD in trend forte.",
|
||||
"⚡", "#00E676",
|
||||
isRecommended: false,
|
||||
cfg =>
|
||||
{
|
||||
cfg.FastEmaPeriod = 5; // EMA veloce 5 periodi (5 minuti)
|
||||
cfg.SlowEmaPeriod = 15; // EMA lenta 15 periodi (15 minuti)
|
||||
cfg.CheckIntervalSeconds = 60; // Tick ogni 60 secondi
|
||||
cfg.AnalysisTimeFrame = Alpaca.Markets.BarTimeFrame.Minute;
|
||||
cfg.HistoricalBarCount = 150; // 2.5 ore di storico
|
||||
cfg.StopLossPercentage = 0.025m; // 2.5% SL
|
||||
cfg.TakeProfitPercentage = 0.05m; // 5% TP
|
||||
}),
|
||||
},
|
||||
|
||||
// ── US Equity ────────────────────────────────────────────────────
|
||||
["us_equity"] = new[]
|
||||
{
|
||||
new StrategyProfile(
|
||||
TradingStrategy.EMA_CROSSOVER,
|
||||
"EMA Crossover",
|
||||
"Strategia trend-following classica, ottimale su azioni con trend chiari.",
|
||||
"📈", "#00E676",
|
||||
isRecommended: true,
|
||||
cfg =>
|
||||
{
|
||||
cfg.FastEmaPeriod = 9;
|
||||
cfg.SlowEmaPeriod = 21;
|
||||
cfg.CheckIntervalSeconds = 60;
|
||||
cfg.StopLossPercentage = 0.02m;
|
||||
cfg.TakeProfitPercentage = 0.04m;
|
||||
}),
|
||||
|
||||
new StrategyProfile(
|
||||
TradingStrategy.MACD,
|
||||
"MACD",
|
||||
"Momentum con istogramma MACD — eccellente su mid/large cap con trend.",
|
||||
"⚡", "#EA80FC",
|
||||
isRecommended: false,
|
||||
cfg =>
|
||||
{
|
||||
cfg.MacdFastPeriod = 12;
|
||||
cfg.MacdSlowPeriod = 26;
|
||||
cfg.MacdSignalPeriod = 9;
|
||||
cfg.CheckIntervalSeconds = 60;
|
||||
cfg.StopLossPercentage = 0.02m;
|
||||
cfg.TakeProfitPercentage = 0.04m;
|
||||
}),
|
||||
|
||||
new StrategyProfile(
|
||||
TradingStrategy.RSI,
|
||||
"RSI Reversal",
|
||||
"Ottimale per azioni in range o in correzione — compra ipervenduto.",
|
||||
"📊", "#FFFF00",
|
||||
isRecommended: false,
|
||||
cfg =>
|
||||
{
|
||||
cfg.RsiPeriod = 14;
|
||||
cfg.RsiOversoldThreshold = 30m;
|
||||
cfg.RsiOverboughtThreshold = 70m;
|
||||
cfg.CheckIntervalSeconds = 120;
|
||||
cfg.StopLossPercentage = 0.02m;
|
||||
cfg.TakeProfitPercentage = 0.035m;
|
||||
}),
|
||||
},
|
||||
|
||||
// ── ETF (mappato come us_equity con parametri diversi) ───────────
|
||||
["etf"] = new[]
|
||||
{
|
||||
new StrategyProfile(
|
||||
TradingStrategy.KALMAN_MEAN_REVERSION,
|
||||
"Kalman Mean Reversion",
|
||||
"ETF tendono a mean-revert: il filtro Kalman ne stima il fair value.",
|
||||
"🔬", "#40C4FF",
|
||||
isRecommended: true,
|
||||
cfg =>
|
||||
{
|
||||
cfg.KalmanDelta = 1e-5;
|
||||
cfg.KalmanObservationVariance = 0.8;
|
||||
cfg.KalmanEntryZScore = 1.8;
|
||||
cfg.KalmanExitZScore = 0.2;
|
||||
cfg.CheckIntervalSeconds = 300;
|
||||
cfg.StopLossPercentage = 0.015m;
|
||||
cfg.TakeProfitPercentage = 0.03m;
|
||||
}),
|
||||
|
||||
new StrategyProfile(
|
||||
TradingStrategy.EMA_CROSSOVER,
|
||||
"EMA Crossover",
|
||||
"Trend-following conservativo su ETF ad alta liquidità.",
|
||||
"📈", "#00E676",
|
||||
isRecommended: false,
|
||||
cfg =>
|
||||
{
|
||||
cfg.FastEmaPeriod = 12;
|
||||
cfg.SlowEmaPeriod = 26;
|
||||
cfg.CheckIntervalSeconds = 120;
|
||||
cfg.StopLossPercentage = 0.015m;
|
||||
cfg.TakeProfitPercentage = 0.03m;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// ── Fallback: usa equity ─────────────────────────────────────────────
|
||||
private static StrategyProfile[] GetProfiles(string assetClass)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetClass)) return _profiles["us_equity"];
|
||||
var key = NormalizeClass(assetClass);
|
||||
return _profiles.TryGetValue(key, out var p) ? p : _profiles["us_equity"];
|
||||
}
|
||||
|
||||
/// <summary>Restituisce tutti i profili strategia disponibili per la classe dell'asset.</summary>
|
||||
public static StrategyProfile[] GetAvailableProfiles(string assetClass)
|
||||
=> GetProfiles(assetClass);
|
||||
|
||||
/// <summary>Restituisce il profilo raccomandato (primo marcato IsRecommended).</summary>
|
||||
public static StrategyProfile GetRecommended(string assetClass)
|
||||
{
|
||||
var profiles = GetProfiles(assetClass);
|
||||
foreach (var p in profiles)
|
||||
if (p.IsRecommended) return p;
|
||||
return profiles[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applica i parametri ottimali del profilo raccomandato alla configurazione.
|
||||
/// Mantiene Symbol, Quantity e Name invariati.
|
||||
/// </summary>
|
||||
public static void ApplyOptimalConfig(BotConfiguration cfg, string assetClass)
|
||||
{
|
||||
var profile = GetRecommended(assetClass);
|
||||
cfg.Strategy = profile.Strategy;
|
||||
profile.ApplyParameters(cfg);
|
||||
}
|
||||
|
||||
private static string NormalizeClass(string assetClass)
|
||||
{
|
||||
// Alpaca restituisce "us_equity", "crypto", ecc.
|
||||
var lower = assetClass.ToLowerInvariant().Replace("-", "_");
|
||||
if (lower.Contains("crypto")) return "crypto";
|
||||
if (lower.Contains("etf")) return "etf";
|
||||
return lower;
|
||||
}
|
||||
}
|
||||
|
||||
// ── StrategyProfile ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Descrive una strategia disponibile per una classe di asset, con i parametri pre-calibrati.</summary>
|
||||
public sealed class StrategyProfile
|
||||
{
|
||||
public TradingStrategy Strategy { get; }
|
||||
public string DisplayName { get; }
|
||||
public string Description { get; }
|
||||
public string Icon { get; }
|
||||
public string AccentColor { get; }
|
||||
public bool IsRecommended { get; }
|
||||
|
||||
private readonly Action<BotConfiguration> _applyParameters;
|
||||
|
||||
public StrategyProfile(
|
||||
TradingStrategy strategy,
|
||||
string displayName,
|
||||
string description,
|
||||
string icon,
|
||||
string accentColor,
|
||||
bool isRecommended,
|
||||
Action<BotConfiguration> applyParameters)
|
||||
{
|
||||
Strategy = strategy;
|
||||
DisplayName = displayName;
|
||||
Description = description;
|
||||
Icon = icon;
|
||||
AccentColor = accentColor;
|
||||
IsRecommended = isRecommended;
|
||||
_applyParameters = applyParameters;
|
||||
}
|
||||
|
||||
public void ApplyParameters(BotConfiguration cfg) => _applyParameters(cfg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user