Nuove: multi-strategy, indicatori avanzati, posizioni
- Sidebar portfolio con metriche dettagliate (Totale, Investito, Disponibile, P&L, ROI) e aggiornamento real-time - Sistema multi-strategia: 8 strategie assegnabili per asset, voting decisionale, pagina Trading Control - Nuova pagina Posizioni: gestione, chiusura manuale, P&L non realizzato, notifiche - Sistema indicatori tecnici: 7+ indicatori configurabili, segnali real-time, raccomandazioni, storico segnali - Refactoring TradingBotService per capitale, P&L, ROI, eventi - Nuovi modelli e servizi per strategie/indicatori, persistenza configurazioni - UI/UX: navigazione aggiornata, widget, modali, responsive - Aggiornamento README e CHANGELOG con tutte le novità
This commit is contained in:
346
TradingBot/Services/IndicatorsService.cs
Normal file
346
TradingBot/Services/IndicatorsService.cs
Normal file
@@ -0,0 +1,346 @@
|
||||
using TradingBot.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TradingBot.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing trading indicators configuration and signals
|
||||
/// </summary>
|
||||
public class IndicatorsService
|
||||
{
|
||||
private readonly Dictionary<string, IndicatorConfig> _indicators = new();
|
||||
private readonly Dictionary<string, Dictionary<string, IndicatorStatus>> _indicatorStatus = new();
|
||||
private readonly List<IndicatorSignal> _recentSignals = new();
|
||||
private readonly string _configPath;
|
||||
private const int MaxSignals = 100;
|
||||
|
||||
public event Action? OnIndicatorsChanged;
|
||||
public event Action<IndicatorSignal>? OnSignalGenerated;
|
||||
|
||||
public IndicatorsService()
|
||||
{
|
||||
_configPath = Path.Combine(Directory.GetCurrentDirectory(), "data", "indicators-config.json");
|
||||
InitializeDefaultIndicators();
|
||||
LoadConfiguration();
|
||||
}
|
||||
|
||||
private void InitializeDefaultIndicators()
|
||||
{
|
||||
_indicators["rsi"] = new IndicatorConfig
|
||||
{
|
||||
Id = "rsi",
|
||||
Name = "RSI",
|
||||
Description = "Relative Strength Index - Misura la forza del trend",
|
||||
Type = IndicatorType.RSI,
|
||||
IsEnabled = true,
|
||||
Period = 14,
|
||||
OverboughtThreshold = 70,
|
||||
OversoldThreshold = 30
|
||||
};
|
||||
|
||||
_indicators["macd"] = new IndicatorConfig
|
||||
{
|
||||
Id = "macd",
|
||||
Name = "MACD",
|
||||
Description = "Moving Average Convergence Divergence - Identifica cambi di trend",
|
||||
Type = IndicatorType.MACD,
|
||||
IsEnabled = true,
|
||||
FastPeriod = 12,
|
||||
SlowPeriod = 26,
|
||||
SignalPeriod = 9
|
||||
};
|
||||
|
||||
_indicators["sma_20"] = new IndicatorConfig
|
||||
{
|
||||
Id = "sma_20",
|
||||
Name = "SMA 20",
|
||||
Description = "Simple Moving Average 20 periodi - Trend a breve termine",
|
||||
Type = IndicatorType.SMA,
|
||||
IsEnabled = true,
|
||||
Period = 20
|
||||
};
|
||||
|
||||
_indicators["sma_50"] = new IndicatorConfig
|
||||
{
|
||||
Id = "sma_50",
|
||||
Name = "SMA 50",
|
||||
Description = "Simple Moving Average 50 periodi - Trend a medio termine",
|
||||
Type = IndicatorType.SMA,
|
||||
IsEnabled = true,
|
||||
Period = 50
|
||||
};
|
||||
|
||||
_indicators["ema_12"] = new IndicatorConfig
|
||||
{
|
||||
Id = "ema_12",
|
||||
Name = "EMA 12",
|
||||
Description = "Exponential Moving Average 12 periodi - Reattivo ai cambiamenti",
|
||||
Type = IndicatorType.EMA,
|
||||
IsEnabled = true,
|
||||
Period = 12
|
||||
};
|
||||
|
||||
_indicators["bollinger"] = new IndicatorConfig
|
||||
{
|
||||
Id = "bollinger",
|
||||
Name = "Bollinger Bands",
|
||||
Description = "Bande di Bollinger - Misura volatilità e livelli estremi",
|
||||
Type = IndicatorType.BollingerBands,
|
||||
IsEnabled = true,
|
||||
Period = 20
|
||||
};
|
||||
|
||||
_indicators["stochastic"] = new IndicatorConfig
|
||||
{
|
||||
Id = "stochastic",
|
||||
Name = "Stochastic",
|
||||
Description = "Oscillatore Stocastico - Identifica momenti di inversione",
|
||||
Type = IndicatorType.Stochastic,
|
||||
IsEnabled = false,
|
||||
Period = 14,
|
||||
OverboughtThreshold = 80,
|
||||
OversoldThreshold = 20
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all indicator configurations
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, IndicatorConfig> GetIndicators()
|
||||
{
|
||||
return _indicators;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get enabled indicators only
|
||||
/// </summary>
|
||||
public IEnumerable<IndicatorConfig> GetEnabledIndicators()
|
||||
{
|
||||
return _indicators.Values.Where(i => i.IsEnabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update indicator configuration
|
||||
/// </summary>
|
||||
public void UpdateIndicator(string id, IndicatorConfig config)
|
||||
{
|
||||
_indicators[id] = config;
|
||||
SaveConfiguration();
|
||||
OnIndicatorsChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle indicator on/off
|
||||
/// </summary>
|
||||
public void ToggleIndicator(string id, bool enabled)
|
||||
{
|
||||
if (_indicators.TryGetValue(id, out var indicator))
|
||||
{
|
||||
indicator.IsEnabled = enabled;
|
||||
SaveConfiguration();
|
||||
OnIndicatorsChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update indicator status for a symbol
|
||||
/// </summary>
|
||||
public void UpdateIndicatorStatus(string indicatorId, string symbol, IndicatorStatus status)
|
||||
{
|
||||
if (!_indicatorStatus.ContainsKey(symbol))
|
||||
{
|
||||
_indicatorStatus[symbol] = new Dictionary<string, IndicatorStatus>();
|
||||
}
|
||||
|
||||
_indicatorStatus[symbol][indicatorId] = status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get indicator status for a symbol
|
||||
/// </summary>
|
||||
public IndicatorStatus? GetIndicatorStatus(string indicatorId, string symbol)
|
||||
{
|
||||
if (_indicatorStatus.TryGetValue(symbol, out var symbolIndicators))
|
||||
{
|
||||
symbolIndicators.TryGetValue(indicatorId, out var status);
|
||||
return status;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all indicator statuses for a symbol
|
||||
/// </summary>
|
||||
public IEnumerable<IndicatorStatus> GetSymbolIndicators(string symbol)
|
||||
{
|
||||
if (_indicatorStatus.TryGetValue(symbol, out var symbolIndicators))
|
||||
{
|
||||
return symbolIndicators.Values;
|
||||
}
|
||||
return Enumerable.Empty<IndicatorStatus>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate and record a signal
|
||||
/// </summary>
|
||||
public void GenerateSignal(IndicatorSignal signal)
|
||||
{
|
||||
_recentSignals.Insert(0, signal);
|
||||
|
||||
// Maintain max size
|
||||
while (_recentSignals.Count > MaxSignals)
|
||||
{
|
||||
_recentSignals.RemoveAt(_recentSignals.Count - 1);
|
||||
}
|
||||
|
||||
OnSignalGenerated?.Invoke(signal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get recent signals
|
||||
/// </summary>
|
||||
public IReadOnlyList<IndicatorSignal> GetRecentSignals(int count = 20)
|
||||
{
|
||||
return _recentSignals.Take(count).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get signals for a specific symbol
|
||||
/// </summary>
|
||||
public IReadOnlyList<IndicatorSignal> GetSymbolSignals(string symbol, int count = 20)
|
||||
{
|
||||
return _recentSignals
|
||||
.Where(s => s.Symbol.Equals(symbol, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(count)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyze indicators and generate trading recommendation
|
||||
/// </summary>
|
||||
public TradingRecommendation AnalyzeIndicators(string symbol)
|
||||
{
|
||||
var recommendation = new TradingRecommendation
|
||||
{
|
||||
Symbol = symbol,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var symbolIndicators = GetSymbolIndicators(symbol).ToList();
|
||||
if (!symbolIndicators.Any())
|
||||
{
|
||||
recommendation.Action = "HOLD";
|
||||
recommendation.Confidence = 0;
|
||||
recommendation.Reason = "Indicatori non disponibili";
|
||||
return recommendation;
|
||||
}
|
||||
|
||||
int buySignals = 0;
|
||||
int sellSignals = 0;
|
||||
int totalEnabled = GetEnabledIndicators().Count();
|
||||
|
||||
foreach (var status in symbolIndicators)
|
||||
{
|
||||
if (!_indicators.TryGetValue(status.IndicatorId, out var config) || !config.IsEnabled)
|
||||
continue;
|
||||
|
||||
switch (status.Condition)
|
||||
{
|
||||
case MarketCondition.Oversold:
|
||||
case MarketCondition.Bullish:
|
||||
buySignals++;
|
||||
recommendation.SupportingIndicators.Add($"{config.Name}: {status.Recommendation}");
|
||||
break;
|
||||
|
||||
case MarketCondition.Overbought:
|
||||
case MarketCondition.Bearish:
|
||||
sellSignals++;
|
||||
recommendation.SupportingIndicators.Add($"{config.Name}: {status.Recommendation}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine action based on signals
|
||||
if (buySignals > sellSignals && buySignals >= totalEnabled * 0.6m)
|
||||
{
|
||||
recommendation.Action = "BUY";
|
||||
recommendation.Confidence = (decimal)buySignals / totalEnabled * 100;
|
||||
recommendation.Reason = $"{buySignals}/{totalEnabled} indicatori suggeriscono acquisto";
|
||||
}
|
||||
else if (sellSignals > buySignals && sellSignals >= totalEnabled * 0.6m)
|
||||
{
|
||||
recommendation.Action = "SELL";
|
||||
recommendation.Confidence = (decimal)sellSignals / totalEnabled * 100;
|
||||
recommendation.Reason = $"{sellSignals}/{totalEnabled} indicatori suggeriscono vendita";
|
||||
}
|
||||
else
|
||||
{
|
||||
recommendation.Action = "HOLD";
|
||||
recommendation.Confidence = 50;
|
||||
recommendation.Reason = "Segnali contrastanti - attendere conferma";
|
||||
}
|
||||
|
||||
return recommendation;
|
||||
}
|
||||
|
||||
private void SaveConfiguration()
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_configPath);
|
||||
if (directory != null && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(_indicators, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
File.WriteAllText(_configPath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error saving indicators configuration: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadConfiguration()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_configPath))
|
||||
{
|
||||
var json = File.ReadAllText(_configPath);
|
||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, IndicatorConfig>>(json);
|
||||
|
||||
if (loaded != null)
|
||||
{
|
||||
foreach (var kvp in loaded)
|
||||
{
|
||||
_indicators[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading indicators configuration: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trading recommendation based on multiple indicators
|
||||
/// </summary>
|
||||
public class TradingRecommendation
|
||||
{
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string Action { get; set; } = "HOLD"; // BUY, SELL, HOLD
|
||||
public decimal Confidence { get; set; }
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
public List<string> SupportingIndicators { get; set; } = new();
|
||||
}
|
||||
@@ -70,4 +70,22 @@ public static class TechnicalAnalysis
|
||||
|
||||
return (macdLine, signalLine, histogram);
|
||||
}
|
||||
|
||||
public static (decimal upper, decimal middle, decimal lower) CalculateBollingerBands(List<decimal> prices, int period = 20, decimal standardDeviations = 2)
|
||||
{
|
||||
if (prices.Count < period) return (0, 0, 0);
|
||||
|
||||
var recentPrices = prices.TakeLast(period).ToList();
|
||||
var sma = recentPrices.Average();
|
||||
|
||||
// Calculate standard deviation
|
||||
var squaredDifferences = recentPrices.Select(p => (double)Math.Pow((double)(p - sma), 2));
|
||||
var variance = squaredDifferences.Average();
|
||||
var stdDev = (decimal)Math.Sqrt(variance);
|
||||
|
||||
var upper = sma + (standardDeviations * stdDev);
|
||||
var lower = sma - (standardDeviations * stdDev);
|
||||
|
||||
return (upper, sma, lower);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ public class TradingBotService
|
||||
private readonly ITradingStrategy _strategy;
|
||||
private readonly TradeHistoryService _historyService;
|
||||
private readonly LoggingService _loggingService;
|
||||
private readonly IndicatorsService _indicatorsService;
|
||||
private readonly TradingStrategiesService _strategiesService;
|
||||
private readonly Dictionary<string, AssetConfiguration> _assetConfigs = new();
|
||||
private readonly Dictionary<string, AssetStatistics> _assetStats = new();
|
||||
private readonly List<Trade> _trades = new();
|
||||
@@ -34,12 +36,16 @@ public class TradingBotService
|
||||
IMarketDataService marketDataService,
|
||||
ITradingStrategy strategy,
|
||||
TradeHistoryService historyService,
|
||||
LoggingService loggingService)
|
||||
LoggingService loggingService,
|
||||
IndicatorsService indicatorsService,
|
||||
TradingStrategiesService strategiesService)
|
||||
{
|
||||
_marketDataService = marketDataService;
|
||||
_strategy = strategy;
|
||||
_historyService = historyService;
|
||||
_loggingService = loggingService;
|
||||
_indicatorsService = indicatorsService;
|
||||
_strategiesService = strategiesService;
|
||||
Status.CurrentStrategy = strategy.Name;
|
||||
|
||||
// Subscribe to simulated market updates if available
|
||||
@@ -484,8 +490,138 @@ public class TradingBotService
|
||||
|
||||
_indicators[symbol] = indicators;
|
||||
OnIndicatorsUpdated?.Invoke(symbol, indicators);
|
||||
|
||||
// Update IndicatorsService statuses
|
||||
UpdateIndicatorStatuses(symbol, indicators, prices);
|
||||
}
|
||||
|
||||
|
||||
private void UpdateIndicatorStatuses(string symbol, TechnicalIndicators indicators, List<decimal> prices)
|
||||
{
|
||||
// Update RSI status
|
||||
var rsiConfig = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "rsi");
|
||||
if (rsiConfig?.IsEnabled == true)
|
||||
{
|
||||
var rsiStatus = new IndicatorStatus
|
||||
{
|
||||
IndicatorId = "rsi",
|
||||
Symbol = symbol,
|
||||
CurrentValue = indicators.RSI,
|
||||
Condition = indicators.RSI > (rsiConfig.OverboughtThreshold ?? 70) ? MarketCondition.Overbought :
|
||||
indicators.RSI < (rsiConfig.OversoldThreshold ?? 30) ? MarketCondition.Oversold :
|
||||
MarketCondition.Neutral,
|
||||
Recommendation = indicators.RSI > (rsiConfig.OverboughtThreshold ?? 70) ? "Possibile vendita" :
|
||||
indicators.RSI < (rsiConfig.OversoldThreshold ?? 30) ? "Possibile acquisto" :
|
||||
"Attendi conferma"
|
||||
};
|
||||
_indicatorsService.UpdateIndicatorStatus("rsi", symbol, rsiStatus);
|
||||
|
||||
// Generate signal if crossing threshold
|
||||
if (indicators.RSI < 30)
|
||||
{
|
||||
_indicatorsService.GenerateSignal(new IndicatorSignal
|
||||
{
|
||||
IndicatorId = "rsi",
|
||||
IndicatorName = "RSI",
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Buy,
|
||||
Strength = indicators.RSI < 20 ? SignalStrength.VeryStrong : SignalStrength.Strong,
|
||||
Message = $"RSI in zona ipervenduto: {indicators.RSI:F2}",
|
||||
Value = indicators.RSI
|
||||
});
|
||||
}
|
||||
else if (indicators.RSI > 70)
|
||||
{
|
||||
_indicatorsService.GenerateSignal(new IndicatorSignal
|
||||
{
|
||||
IndicatorId = "rsi",
|
||||
IndicatorName = "RSI",
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Sell,
|
||||
Strength = indicators.RSI > 80 ? SignalStrength.VeryStrong : SignalStrength.Strong,
|
||||
Message = $"RSI in zona ipercomprato: {indicators.RSI:F2}",
|
||||
Value = indicators.RSI
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update MACD status
|
||||
var macdConfig = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "macd");
|
||||
if (macdConfig?.IsEnabled == true)
|
||||
{
|
||||
var macdStatus = new IndicatorStatus
|
||||
{
|
||||
IndicatorId = "macd",
|
||||
Symbol = symbol,
|
||||
CurrentValue = indicators.MACD,
|
||||
Condition = indicators.Histogram > 0 ? MarketCondition.Bullish : MarketCondition.Bearish,
|
||||
Recommendation = indicators.Histogram > 0 ? "Trend rialzista" : "Trend ribassista"
|
||||
};
|
||||
_indicatorsService.UpdateIndicatorStatus("macd", symbol, macdStatus);
|
||||
|
||||
// Generate signal on crossover
|
||||
if (Math.Abs(indicators.Histogram) < 0.5m) // Near crossover
|
||||
{
|
||||
_indicatorsService.GenerateSignal(new IndicatorSignal
|
||||
{
|
||||
IndicatorId = "macd",
|
||||
IndicatorName = "MACD",
|
||||
Symbol = symbol,
|
||||
Type = indicators.Histogram > 0 ? SignalType.Buy : SignalType.Sell,
|
||||
Strength = SignalStrength.Moderate,
|
||||
Message = $"MACD {(indicators.Histogram > 0 ? "bullish" : "bearish")} crossover",
|
||||
Value = indicators.MACD
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update SMA statuses
|
||||
var currentPrice = prices.Last();
|
||||
var sma20Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "sma_20");
|
||||
if (sma20Config?.IsEnabled == true && prices.Count >= 20)
|
||||
{
|
||||
var sma20 = prices.TakeLast(20).Average();
|
||||
var sma20Status = new IndicatorStatus
|
||||
{
|
||||
IndicatorId = "sma_20",
|
||||
Symbol = symbol,
|
||||
CurrentValue = sma20,
|
||||
Condition = currentPrice > sma20 ? MarketCondition.Bullish : MarketCondition.Bearish,
|
||||
Recommendation = currentPrice > sma20 ? "Prezzo sopra media" : "Prezzo sotto media"
|
||||
};
|
||||
_indicatorsService.UpdateIndicatorStatus("sma_20", symbol, sma20Status);
|
||||
}
|
||||
|
||||
var sma50Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "sma_50");
|
||||
if (sma50Config?.IsEnabled == true && prices.Count >= 50)
|
||||
{
|
||||
var sma50 = prices.TakeLast(50).Average();
|
||||
var sma50Status = new IndicatorStatus
|
||||
{
|
||||
IndicatorId = "sma_50",
|
||||
Symbol = symbol,
|
||||
CurrentValue = sma50,
|
||||
Condition = currentPrice > sma50 ? MarketCondition.Bullish : MarketCondition.Bearish,
|
||||
Recommendation = currentPrice > sma50 ? "Trend rialzista medio termine" : "Trend ribassista medio termine"
|
||||
};
|
||||
_indicatorsService.UpdateIndicatorStatus("sma_50", symbol, sma50Status);
|
||||
}
|
||||
|
||||
// Update EMA status
|
||||
var ema12Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "ema_12");
|
||||
if (ema12Config?.IsEnabled == true)
|
||||
{
|
||||
var ema12Status = new IndicatorStatus
|
||||
{
|
||||
IndicatorId = "ema_12",
|
||||
Symbol = symbol,
|
||||
CurrentValue = indicators.EMA12,
|
||||
Condition = currentPrice > indicators.EMA12 ? MarketCondition.Bullish : MarketCondition.Bearish,
|
||||
Recommendation = currentPrice > indicators.EMA12 ? "Trend positivo" : "Trend negativo"
|
||||
};
|
||||
_indicatorsService.UpdateIndicatorStatus("ema_12", symbol, ema12Status);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAssetStatistics(string symbol, Trade trade, decimal? realizedProfit = null)
|
||||
{
|
||||
if (!_assetStats.TryGetValue(symbol, out var stats))
|
||||
@@ -631,4 +767,30 @@ public class TradingBotService
|
||||
OnStatusChanged?.Invoke();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually close a position
|
||||
/// </summary>
|
||||
public async Task ClosePositionManuallyAsync(string symbol)
|
||||
{
|
||||
if (!_activePositions.TryGetValue(symbol, out var position))
|
||||
{
|
||||
throw new InvalidOperationException($"No active position found for {symbol}");
|
||||
}
|
||||
|
||||
if (!_assetConfigs.TryGetValue(symbol, out var config))
|
||||
{
|
||||
throw new InvalidOperationException($"Asset configuration not found for {symbol}");
|
||||
}
|
||||
|
||||
// Get current market price
|
||||
var latestPrice = GetLatestPrice(symbol);
|
||||
if (latestPrice == null || latestPrice.Price <= 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot get current price for {symbol}");
|
||||
}
|
||||
|
||||
// Execute sell
|
||||
await ExecuteSellAsync(symbol, latestPrice.Price, config.CurrentHoldings, config);
|
||||
}
|
||||
}
|
||||
|
||||
564
TradingBot/Services/TradingStrategies.cs
Normal file
564
TradingBot/Services/TradingStrategies.cs
Normal file
@@ -0,0 +1,564 @@
|
||||
using TradingBot.Models;
|
||||
|
||||
namespace TradingBot.Services;
|
||||
|
||||
/// <summary>
|
||||
/// RSI-based trading strategy
|
||||
/// Buy when RSI < oversold threshold, Sell when RSI > overbought threshold
|
||||
/// </summary>
|
||||
public class RSIStrategy : ITradingStrategy
|
||||
{
|
||||
public string Name => "RSI Strategy";
|
||||
public string Description => "Strategia basata su Relative Strength Index. Compra in zona ipervenduto, vende in zona ipercomprato.";
|
||||
|
||||
private readonly decimal _oversoldThreshold;
|
||||
private readonly decimal _overboughtThreshold;
|
||||
private readonly int _period;
|
||||
|
||||
public RSIStrategy(decimal oversoldThreshold = 30, decimal overboughtThreshold = 70, int period = 14)
|
||||
{
|
||||
_oversoldThreshold = oversoldThreshold;
|
||||
_overboughtThreshold = overboughtThreshold;
|
||||
_period = period;
|
||||
}
|
||||
|
||||
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
|
||||
{
|
||||
if (priceHistory == null || priceHistory.Count < _period + 1)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 0,
|
||||
Reason = "Dati insufficienti per RSI"
|
||||
});
|
||||
}
|
||||
|
||||
var prices = priceHistory.Select(p => p.Price).ToList();
|
||||
var rsi = TechnicalAnalysis.CalculateRSI(prices, _period);
|
||||
|
||||
if (rsi < _oversoldThreshold)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Buy,
|
||||
Confidence = (decimal)(((_oversoldThreshold - rsi) / _oversoldThreshold) * 100),
|
||||
Reason = $"RSI in zona ipervenduto: {rsi:F2}"
|
||||
});
|
||||
}
|
||||
else if (rsi > _overboughtThreshold)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Sell,
|
||||
Confidence = (decimal)(((rsi - _overboughtThreshold) / (100 - _overboughtThreshold)) * 100),
|
||||
Reason = $"RSI in zona ipercomprato: {rsi:F2}"
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 50,
|
||||
Reason = $"RSI neutro: {rsi:F2}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MACD-based trading strategy
|
||||
/// Buy on bullish crossover, Sell on bearish crossover
|
||||
/// </summary>
|
||||
public class MACDStrategy : ITradingStrategy
|
||||
{
|
||||
public string Name => "MACD Strategy";
|
||||
public string Description => "Strategia basata su MACD crossover. Compra su incrocio rialzista, vende su incrocio ribassista.";
|
||||
|
||||
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
|
||||
{
|
||||
if (priceHistory == null || priceHistory.Count < 26)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 0,
|
||||
Reason = "Dati insufficienti per MACD"
|
||||
});
|
||||
}
|
||||
|
||||
var prices = priceHistory.Select(p => p.Price).ToList();
|
||||
var (macd, signal, histogram) = TechnicalAnalysis.CalculateMACD(prices);
|
||||
|
||||
if (histogram > 0 && Math.Abs(histogram) > 0.1m)
|
||||
{
|
||||
var confidence = Math.Min((decimal)(Math.Abs((double)histogram) * 10), 100);
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Buy,
|
||||
Confidence = confidence,
|
||||
Reason = $"MACD crossover rialzista, histogram: {histogram:F2}"
|
||||
});
|
||||
}
|
||||
else if (histogram < 0 && Math.Abs(histogram) > 0.1m)
|
||||
{
|
||||
var confidence = Math.Min((decimal)(Math.Abs((double)histogram) * 10), 100);
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Sell,
|
||||
Confidence = confidence,
|
||||
Reason = $"MACD crossover ribassista, histogram: {histogram:F2}"
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 30,
|
||||
Reason = "MACD vicino a equilibrio"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bollinger Bands strategy
|
||||
/// Buy when price touches lower band, Sell when price touches upper band
|
||||
/// </summary>
|
||||
public class BollingerBandsStrategy : ITradingStrategy
|
||||
{
|
||||
public string Name => "Bollinger Bands";
|
||||
public string Description => "Compra quando il prezzo tocca la banda inferiore, vende alla banda superiore.";
|
||||
|
||||
private readonly int _period;
|
||||
private readonly decimal _standardDeviations;
|
||||
|
||||
public BollingerBandsStrategy(int period = 20, decimal standardDeviations = 2)
|
||||
{
|
||||
_period = period;
|
||||
_standardDeviations = standardDeviations;
|
||||
}
|
||||
|
||||
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
|
||||
{
|
||||
if (priceHistory == null || priceHistory.Count < _period)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 0,
|
||||
Reason = "Dati insufficienti per Bollinger Bands"
|
||||
});
|
||||
}
|
||||
|
||||
var prices = priceHistory.Select(p => p.Price).ToList();
|
||||
var (upper, middle, lower) = TechnicalAnalysis.CalculateBollingerBands(prices, _period, _standardDeviations);
|
||||
var currentPrice = prices.Last();
|
||||
|
||||
var distanceToLower = ((currentPrice - lower) / lower) * 100;
|
||||
var distanceToUpper = ((upper - currentPrice) / upper) * 100;
|
||||
|
||||
if (distanceToLower < 2) // Within 2% of lower band
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Buy,
|
||||
Confidence = 80,
|
||||
Reason = $"Prezzo vicino banda inferiore: ${currentPrice:F2} vs ${lower:F2}"
|
||||
});
|
||||
}
|
||||
else if (distanceToUpper < 2) // Within 2% of upper band
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Sell,
|
||||
Confidence = 80,
|
||||
Reason = $"Prezzo vicino banda superiore: ${currentPrice:F2} vs ${upper:F2}"
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 40,
|
||||
Reason = "Prezzo tra le bande"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mean Reversion strategy
|
||||
/// Assumes price will return to average
|
||||
/// </summary>
|
||||
public class MeanReversionStrategy : ITradingStrategy
|
||||
{
|
||||
public string Name => "Mean Reversion";
|
||||
public string Description => "Sfrutta il ritorno del prezzo verso la media. Compra sotto media, vende sopra media.";
|
||||
|
||||
private readonly int _period;
|
||||
private readonly decimal _deviationThreshold;
|
||||
|
||||
public MeanReversionStrategy(int period = 20, decimal deviationThreshold = 5)
|
||||
{
|
||||
_period = period;
|
||||
_deviationThreshold = deviationThreshold;
|
||||
}
|
||||
|
||||
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
|
||||
{
|
||||
if (priceHistory == null || priceHistory.Count < _period)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 0,
|
||||
Reason = "Dati insufficienti"
|
||||
});
|
||||
}
|
||||
|
||||
var prices = priceHistory.Select(p => p.Price).TakeLast(_period).ToList();
|
||||
var mean = prices.Average();
|
||||
var currentPrice = prices.Last();
|
||||
var deviation = ((currentPrice - mean) / mean) * 100;
|
||||
|
||||
if (deviation < -_deviationThreshold)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Buy,
|
||||
Confidence = Math.Min((decimal)Math.Abs((double)deviation) * 10, 100),
|
||||
Reason = $"Prezzo {deviation:F2}% sotto media, probabile rimbalzo"
|
||||
});
|
||||
}
|
||||
else if (deviation > _deviationThreshold)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Sell,
|
||||
Confidence = Math.Min((decimal)Math.Abs((double)deviation) * 10, 100),
|
||||
Reason = $"Prezzo {deviation:F2}% sopra media, probabile correzione"
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 50,
|
||||
Reason = "Prezzo vicino alla media"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Momentum strategy
|
||||
/// Follows strong trends
|
||||
/// </summary>
|
||||
public class MomentumStrategy : ITradingStrategy
|
||||
{
|
||||
public string Name => "Momentum";
|
||||
public string Description => "Segue i trend forti. Compra su momentum positivo, vende su momentum negativo.";
|
||||
|
||||
private readonly int _period;
|
||||
|
||||
public MomentumStrategy(int period = 10)
|
||||
{
|
||||
_period = period;
|
||||
}
|
||||
|
||||
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
|
||||
{
|
||||
if (priceHistory == null || priceHistory.Count < _period + 5)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 0,
|
||||
Reason = "Dati insufficienti"
|
||||
});
|
||||
}
|
||||
|
||||
var prices = priceHistory.Select(p => p.Price).ToList();
|
||||
var currentPrice = prices.Last();
|
||||
var pastPrice = prices[^_period];
|
||||
var momentum = ((currentPrice - pastPrice) / pastPrice) * 100;
|
||||
|
||||
// Calculate rate of change
|
||||
var recentPrices = prices.TakeLast(5).ToList();
|
||||
var priceChanges = new List<decimal>();
|
||||
for (int i = 1; i < recentPrices.Count; i++)
|
||||
{
|
||||
priceChanges.Add(((recentPrices[i] - recentPrices[i - 1]) / recentPrices[i - 1]) * 100);
|
||||
}
|
||||
var avgChange = priceChanges.Average();
|
||||
|
||||
if (momentum > 3 && avgChange > 0)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Buy,
|
||||
Confidence = Math.Min((decimal)Math.Abs((double)momentum) * 15, 100),
|
||||
Reason = $"Forte momentum positivo: {momentum:F2}%"
|
||||
});
|
||||
}
|
||||
else if (momentum < -3 && avgChange < 0)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Sell,
|
||||
Confidence = Math.Min((decimal)Math.Abs((double)momentum) * 15, 100),
|
||||
Reason = $"Forte momentum negativo: {momentum:F2}%"
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 30,
|
||||
Reason = "Momentum debole o neutro"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EMA Crossover strategy (Golden Cross / Death Cross)
|
||||
/// Buy when fast EMA crosses above slow EMA, Sell on opposite
|
||||
/// </summary>
|
||||
public class EMACrossoverStrategy : ITradingStrategy
|
||||
{
|
||||
public string Name => "EMA Crossover";
|
||||
public string Description => "Golden Cross/Death Cross. Compra quando EMA veloce supera EMA lenta.";
|
||||
|
||||
private readonly int _fastPeriod;
|
||||
private readonly int _slowPeriod;
|
||||
|
||||
public EMACrossoverStrategy(int fastPeriod = 12, int slowPeriod = 26)
|
||||
{
|
||||
_fastPeriod = fastPeriod;
|
||||
_slowPeriod = slowPeriod;
|
||||
}
|
||||
|
||||
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
|
||||
{
|
||||
if (priceHistory == null || priceHistory.Count < _slowPeriod + 5)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 0,
|
||||
Reason = "Dati insufficienti"
|
||||
});
|
||||
}
|
||||
|
||||
var prices = priceHistory.Select(p => p.Price).ToList();
|
||||
var fastEMA = TechnicalAnalysis.CalculateEMA(prices, _fastPeriod);
|
||||
var slowEMA = TechnicalAnalysis.CalculateEMA(prices, _slowPeriod);
|
||||
|
||||
// Calculate previous EMAs to detect crossover
|
||||
var prevPrices = prices.Take(prices.Count - 1).ToList();
|
||||
var prevFastEMA = TechnicalAnalysis.CalculateEMA(prevPrices, _fastPeriod);
|
||||
var prevSlowEMA = TechnicalAnalysis.CalculateEMA(prevPrices, _slowPeriod);
|
||||
|
||||
var currentDiff = fastEMA - slowEMA;
|
||||
var prevDiff = prevFastEMA - prevSlowEMA;
|
||||
|
||||
// Golden Cross (bullish)
|
||||
if (currentDiff > 0 && prevDiff <= 0)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Buy,
|
||||
Confidence = 85,
|
||||
Reason = $"Golden Cross! EMA{_fastPeriod} crossed above EMA{_slowPeriod}"
|
||||
});
|
||||
}
|
||||
// Death Cross (bearish)
|
||||
else if (currentDiff < 0 && prevDiff >= 0)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Sell,
|
||||
Confidence = 85,
|
||||
Reason = $"Death Cross! EMA{_fastPeriod} crossed below EMA{_slowPeriod}"
|
||||
});
|
||||
}
|
||||
// Trend continuation
|
||||
else if (currentDiff > 0)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 60,
|
||||
Reason = "EMA fast sopra slow - trend rialzista confermato"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 40,
|
||||
Reason = "EMA fast sotto slow - trend ribassista confermato"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scalping strategy for short-term gains
|
||||
/// </summary>
|
||||
public class ScalpingStrategy : ITradingStrategy
|
||||
{
|
||||
public string Name => "Scalping";
|
||||
public string Description => "Strategia per guadagni rapidi a breve termine. Alta frequenza, piccoli profitti.";
|
||||
|
||||
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
|
||||
{
|
||||
if (priceHistory == null || priceHistory.Count < 10)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 0,
|
||||
Reason = "Dati insufficienti"
|
||||
});
|
||||
}
|
||||
|
||||
var recentPrices = priceHistory.Select(p => p.Price).TakeLast(10).ToList();
|
||||
var currentPrice = recentPrices.Last();
|
||||
var shortMA = recentPrices.TakeLast(3).Average();
|
||||
var mediumMA = recentPrices.TakeLast(7).Average();
|
||||
|
||||
// Calculate short-term volatility
|
||||
var priceChanges = new List<decimal>();
|
||||
for (int i = 1; i < recentPrices.Count; i++)
|
||||
{
|
||||
priceChanges.Add(Math.Abs(recentPrices[i] - recentPrices[i - 1]));
|
||||
}
|
||||
var avgVolatility = priceChanges.Average();
|
||||
var recentChange = Math.Abs(currentPrice - recentPrices[^2]);
|
||||
|
||||
// Quick reversal detection
|
||||
if (currentPrice < shortMA && shortMA < mediumMA && recentChange > avgVolatility * 1.5m)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Buy,
|
||||
Confidence = 70,
|
||||
Reason = "Possibile rimbalzo rapido"
|
||||
});
|
||||
}
|
||||
else if (currentPrice > shortMA && shortMA > mediumMA && recentChange > avgVolatility * 1.5m)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Sell,
|
||||
Confidence = 70,
|
||||
Reason = "Possibile correzione rapida"
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 30,
|
||||
Reason = "Attesa opportunità scalping"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Breakout strategy
|
||||
/// Trades on price breaking resistance/support levels
|
||||
/// </summary>
|
||||
public class BreakoutStrategy : ITradingStrategy
|
||||
{
|
||||
public string Name => "Breakout";
|
||||
public string Description => "Compra su rottura resistenza, vende su rottura supporto. Cattura breakout significativi.";
|
||||
|
||||
private readonly int _lookbackPeriod;
|
||||
|
||||
public BreakoutStrategy(int lookbackPeriod = 20)
|
||||
{
|
||||
_lookbackPeriod = lookbackPeriod;
|
||||
}
|
||||
|
||||
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
|
||||
{
|
||||
if (priceHistory == null || priceHistory.Count < _lookbackPeriod)
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 0,
|
||||
Reason = "Dati insufficienti"
|
||||
});
|
||||
}
|
||||
|
||||
var prices = priceHistory.Select(p => p.Price).ToList();
|
||||
var recentPrices = prices.TakeLast(_lookbackPeriod).ToList();
|
||||
var currentPrice = prices.Last();
|
||||
|
||||
var resistance = recentPrices.Max();
|
||||
var support = recentPrices.Min();
|
||||
var range = resistance - support;
|
||||
|
||||
// Breakout above resistance
|
||||
if (currentPrice > resistance * 1.01m) // 1% above previous high
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Buy,
|
||||
Confidence = 80,
|
||||
Reason = $"Breakout sopra resistenza: ${resistance:F2}"
|
||||
});
|
||||
}
|
||||
// Breakdown below support
|
||||
else if (currentPrice < support * 0.99m) // 1% below previous low
|
||||
{
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Sell,
|
||||
Confidence = 80,
|
||||
Reason = $"Breakdown sotto supporto: ${support:F2}"
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(new TradingSignal
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Hold,
|
||||
Confidence = 40,
|
||||
Reason = $"Prezzo in range ${support:F2} - ${resistance:F2}"
|
||||
});
|
||||
}
|
||||
}
|
||||
486
TradingBot/Services/TradingStrategiesService.cs
Normal file
486
TradingBot/Services/TradingStrategiesService.cs
Normal file
@@ -0,0 +1,486 @@
|
||||
using TradingBot.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TradingBot.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing trading strategies and their assignments to assets
|
||||
/// </summary>
|
||||
public class TradingStrategiesService
|
||||
{
|
||||
private readonly Dictionary<string, StrategyInfo> _availableStrategies = new();
|
||||
private readonly Dictionary<string, ITradingStrategy> _strategyInstances = new();
|
||||
private readonly Dictionary<string, AssetStrategyMapping> _assetMappings = new();
|
||||
private readonly Dictionary<string, TradingEngineStatus> _engineStatuses = new();
|
||||
private readonly string _configPath;
|
||||
|
||||
public event Action? OnMappingsChanged;
|
||||
public event Action<string, TradingDecision>? OnDecisionMade;
|
||||
|
||||
public TradingStrategiesService()
|
||||
{
|
||||
_configPath = Path.Combine(Directory.GetCurrentDirectory(), "data", "strategy-mappings.json");
|
||||
InitializeStrategies();
|
||||
LoadMappings();
|
||||
}
|
||||
|
||||
private void InitializeStrategies()
|
||||
{
|
||||
// RSI Strategy
|
||||
var rsiStrategy = new RSIStrategy();
|
||||
_strategyInstances["rsi"] = rsiStrategy;
|
||||
_availableStrategies["rsi"] = new StrategyInfo
|
||||
{
|
||||
Id = "rsi",
|
||||
Name = "RSI Strategy",
|
||||
Description = "Relative Strength Index - Compra in ipervenduto, vende in ipercomprato",
|
||||
Category = "Oscillator",
|
||||
RiskLevel = StrategyRisk.Medium,
|
||||
RecommendedTimeFrame = TimeFrame.ShortTerm,
|
||||
RequiredIndicators = new List<string> { "RSI" },
|
||||
Parameters = new Dictionary<string, ParameterInfo>
|
||||
{
|
||||
["oversoldThreshold"] = new() { Name = "Oversold", Description = "Soglia ipervenduto", Type = ParameterType.Decimal, DefaultValue = 30m, MinValue = 10m, MaxValue = 40m },
|
||||
["overboughtThreshold"] = new() { Name = "Overbought", Description = "Soglia ipercomprato", Type = ParameterType.Decimal, DefaultValue = 70m, MinValue = 60m, MaxValue = 90m },
|
||||
["period"] = new() { Name = "Period", Description = "Periodo di calcolo", Type = ParameterType.Integer, DefaultValue = 14, MinValue = 5, MaxValue = 30 }
|
||||
}
|
||||
};
|
||||
|
||||
// MACD Strategy
|
||||
var macdStrategy = new MACDStrategy();
|
||||
_strategyInstances["macd"] = macdStrategy;
|
||||
_availableStrategies["macd"] = new StrategyInfo
|
||||
{
|
||||
Id = "macd",
|
||||
Name = "MACD Strategy",
|
||||
Description = "Moving Average Convergence Divergence - Crossover rialzista/ribassista",
|
||||
Category = "Momentum",
|
||||
RiskLevel = StrategyRisk.Medium,
|
||||
RecommendedTimeFrame = TimeFrame.MediumTerm,
|
||||
RequiredIndicators = new List<string> { "MACD", "Signal", "Histogram" }
|
||||
};
|
||||
|
||||
// Bollinger Bands Strategy
|
||||
var bollingerStrategy = new BollingerBandsStrategy();
|
||||
_strategyInstances["bollinger"] = bollingerStrategy;
|
||||
_availableStrategies["bollinger"] = new StrategyInfo
|
||||
{
|
||||
Id = "bollinger",
|
||||
Name = "Bollinger Bands",
|
||||
Description = "Compra vicino banda inferiore, vende vicino banda superiore",
|
||||
Category = "Volatility",
|
||||
RiskLevel = StrategyRisk.Low,
|
||||
RecommendedTimeFrame = TimeFrame.MediumTerm,
|
||||
RequiredIndicators = new List<string> { "Bollinger Bands" },
|
||||
Parameters = new Dictionary<string, ParameterInfo>
|
||||
{
|
||||
["period"] = new() { Name = "Period", Description = "Periodo SMA", Type = ParameterType.Integer, DefaultValue = 20, MinValue = 10, MaxValue = 50 },
|
||||
["standardDeviations"] = new() { Name = "Std Dev", Description = "Deviazioni standard", Type = ParameterType.Decimal, DefaultValue = 2m, MinValue = 1m, MaxValue = 3m }
|
||||
}
|
||||
};
|
||||
|
||||
// Mean Reversion Strategy
|
||||
var meanReversionStrategy = new MeanReversionStrategy();
|
||||
_strategyInstances["mean_reversion"] = meanReversionStrategy;
|
||||
_availableStrategies["mean_reversion"] = new StrategyInfo
|
||||
{
|
||||
Id = "mean_reversion",
|
||||
Name = "Mean Reversion",
|
||||
Description = "Sfrutta il ritorno del prezzo verso la media",
|
||||
Category = "Contrarian",
|
||||
RiskLevel = StrategyRisk.High,
|
||||
RecommendedTimeFrame = TimeFrame.ShortTerm,
|
||||
RequiredIndicators = new List<string> { "SMA" },
|
||||
Parameters = new Dictionary<string, ParameterInfo>
|
||||
{
|
||||
["period"] = new() { Name = "Period", Description = "Periodo media", Type = ParameterType.Integer, DefaultValue = 20, MinValue = 10, MaxValue = 50 },
|
||||
["deviationThreshold"] = new() { Name = "Deviation %", Description = "Soglia deviazione", Type = ParameterType.Decimal, DefaultValue = 5m, MinValue = 2m, MaxValue = 10m }
|
||||
}
|
||||
};
|
||||
|
||||
// Momentum Strategy
|
||||
var momentumStrategy = new MomentumStrategy();
|
||||
_strategyInstances["momentum"] = momentumStrategy;
|
||||
_availableStrategies["momentum"] = new StrategyInfo
|
||||
{
|
||||
Id = "momentum",
|
||||
Name = "Momentum",
|
||||
Description = "Segue i trend forti basati su momentum",
|
||||
Category = "Trend",
|
||||
RiskLevel = StrategyRisk.Medium,
|
||||
RecommendedTimeFrame = TimeFrame.MediumTerm,
|
||||
RequiredIndicators = new List<string> { "Price Change" },
|
||||
Parameters = new Dictionary<string, ParameterInfo>
|
||||
{
|
||||
["period"] = new() { Name = "Period", Description = "Periodo momentum", Type = ParameterType.Integer, DefaultValue = 10, MinValue = 5, MaxValue = 20 }
|
||||
}
|
||||
};
|
||||
|
||||
// EMA Crossover Strategy
|
||||
var emaCrossoverStrategy = new EMACrossoverStrategy();
|
||||
_strategyInstances["ema_crossover"] = emaCrossoverStrategy;
|
||||
_availableStrategies["ema_crossover"] = new StrategyInfo
|
||||
{
|
||||
Id = "ema_crossover",
|
||||
Name = "EMA Crossover",
|
||||
Description = "Golden Cross/Death Cross con EMA",
|
||||
Category = "Trend",
|
||||
RiskLevel = StrategyRisk.Low,
|
||||
RecommendedTimeFrame = TimeFrame.LongTerm,
|
||||
RequiredIndicators = new List<string> { "EMA12", "EMA26" },
|
||||
Parameters = new Dictionary<string, ParameterInfo>
|
||||
{
|
||||
["fastPeriod"] = new() { Name = "Fast EMA", Description = "Periodo EMA veloce", Type = ParameterType.Integer, DefaultValue = 12, MinValue = 8, MaxValue = 20 },
|
||||
["slowPeriod"] = new() { Name = "Slow EMA", Description = "Periodo EMA lenta", Type = ParameterType.Integer, DefaultValue = 26, MinValue = 20, MaxValue = 50 }
|
||||
}
|
||||
};
|
||||
|
||||
// Scalping Strategy
|
||||
var scalpingStrategy = new ScalpingStrategy();
|
||||
_strategyInstances["scalping"] = scalpingStrategy;
|
||||
_availableStrategies["scalping"] = new StrategyInfo
|
||||
{
|
||||
Id = "scalping",
|
||||
Name = "Scalping",
|
||||
Description = "Guadagni rapidi a breve termine",
|
||||
Category = "Short-term",
|
||||
RiskLevel = StrategyRisk.VeryHigh,
|
||||
RecommendedTimeFrame = TimeFrame.ShortTerm,
|
||||
RequiredIndicators = new List<string> { "Short MA", "Volatility" }
|
||||
};
|
||||
|
||||
// Breakout Strategy
|
||||
var breakoutStrategy = new BreakoutStrategy();
|
||||
_strategyInstances["breakout"] = breakoutStrategy;
|
||||
_availableStrategies["breakout"] = new StrategyInfo
|
||||
{
|
||||
Id = "breakout",
|
||||
Name = "Breakout",
|
||||
Description = "Cattura rotture di resistenza/supporto",
|
||||
Category = "Volatility",
|
||||
RiskLevel = StrategyRisk.High,
|
||||
RecommendedTimeFrame = TimeFrame.MediumTerm,
|
||||
RequiredIndicators = new List<string> { "Resistance", "Support" },
|
||||
Parameters = new Dictionary<string, ParameterInfo>
|
||||
{
|
||||
["lookbackPeriod"] = new() { Name = "Lookback", Description = "Periodo lookback", Type = ParameterType.Integer, DefaultValue = 20, MinValue = 10, MaxValue = 50 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all available strategies
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, StrategyInfo> GetAvailableStrategies()
|
||||
{
|
||||
return _availableStrategies;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get strategies by category
|
||||
/// </summary>
|
||||
public IEnumerable<StrategyInfo> GetStrategiesByCategory(string category)
|
||||
{
|
||||
return _availableStrategies.Values.Where(s => s.Category == category);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get asset mapping
|
||||
/// </summary>
|
||||
public AssetStrategyMapping? GetAssetMapping(string symbol)
|
||||
{
|
||||
_assetMappings.TryGetValue(symbol, out var mapping);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all asset mappings
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, AssetStrategyMapping> GetAllMappings()
|
||||
{
|
||||
return _assetMappings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assign strategies to an asset
|
||||
/// </summary>
|
||||
public void AssignStrategiesToAsset(string symbol, string assetName, List<string> strategyIds)
|
||||
{
|
||||
var mapping = new AssetStrategyMapping
|
||||
{
|
||||
Symbol = symbol,
|
||||
AssetName = assetName,
|
||||
StrategyIds = strategyIds,
|
||||
IsActive = false,
|
||||
ActivatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_assetMappings[symbol] = mapping;
|
||||
|
||||
// Initialize engine status
|
||||
if (!_engineStatuses.ContainsKey(symbol))
|
||||
{
|
||||
_engineStatuses[symbol] = new TradingEngineStatus
|
||||
{
|
||||
Symbol = symbol,
|
||||
IsRunning = false,
|
||||
ActiveStrategies = 0
|
||||
};
|
||||
}
|
||||
|
||||
SaveMappings();
|
||||
OnMappingsChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove strategy from asset
|
||||
/// </summary>
|
||||
public void RemoveStrategyFromAsset(string symbol, string strategyId)
|
||||
{
|
||||
if (_assetMappings.TryGetValue(symbol, out var mapping))
|
||||
{
|
||||
mapping.StrategyIds.Remove(strategyId);
|
||||
if (mapping.StrategyIds.Count == 0)
|
||||
{
|
||||
mapping.IsActive = false;
|
||||
}
|
||||
SaveMappings();
|
||||
OnMappingsChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activate trading for an asset
|
||||
/// </summary>
|
||||
public void ActivateAsset(string symbol)
|
||||
{
|
||||
if (_assetMappings.TryGetValue(symbol, out var mapping) && mapping.StrategyIds.Count > 0)
|
||||
{
|
||||
mapping.IsActive = true;
|
||||
mapping.ActivatedAt = DateTime.UtcNow;
|
||||
mapping.DeactivatedAt = null;
|
||||
|
||||
if (_engineStatuses.TryGetValue(symbol, out var status))
|
||||
{
|
||||
status.IsRunning = true;
|
||||
status.ActiveStrategies = mapping.StrategyIds.Count;
|
||||
}
|
||||
|
||||
SaveMappings();
|
||||
OnMappingsChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivate trading for an asset
|
||||
/// </summary>
|
||||
public void DeactivateAsset(string symbol)
|
||||
{
|
||||
if (_assetMappings.TryGetValue(symbol, out var mapping))
|
||||
{
|
||||
mapping.IsActive = false;
|
||||
mapping.DeactivatedAt = DateTime.UtcNow;
|
||||
|
||||
if (_engineStatuses.TryGetValue(symbol, out var status))
|
||||
{
|
||||
status.IsRunning = false;
|
||||
}
|
||||
|
||||
SaveMappings();
|
||||
OnMappingsChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyze market with assigned strategies
|
||||
/// </summary>
|
||||
public async Task<TradingDecision> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
|
||||
{
|
||||
if (!_assetMappings.TryGetValue(symbol, out var mapping) || !mapping.IsActive)
|
||||
{
|
||||
return new TradingDecision
|
||||
{
|
||||
Symbol = symbol,
|
||||
Decision = SignalType.Hold,
|
||||
Confidence = 0,
|
||||
Reason = "Trading non attivo per questo asset"
|
||||
};
|
||||
}
|
||||
|
||||
var signals = new List<StrategySignal>();
|
||||
int buyVotes = 0, sellVotes = 0, holdVotes = 0;
|
||||
decimal totalConfidence = 0;
|
||||
|
||||
// Execute all assigned strategies
|
||||
foreach (var strategyId in mapping.StrategyIds)
|
||||
{
|
||||
if (_strategyInstances.TryGetValue(strategyId, out var strategy))
|
||||
{
|
||||
var signal = await strategy.AnalyzeAsync(symbol, priceHistory);
|
||||
|
||||
var strategySignal = new StrategySignal
|
||||
{
|
||||
StrategyId = strategyId,
|
||||
StrategyName = _availableStrategies[strategyId].Name,
|
||||
Signal = signal,
|
||||
GeneratedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
signals.Add(strategySignal);
|
||||
|
||||
switch (signal.Type)
|
||||
{
|
||||
case SignalType.Buy:
|
||||
buyVotes++;
|
||||
break;
|
||||
case SignalType.Sell:
|
||||
sellVotes++;
|
||||
break;
|
||||
case SignalType.Hold:
|
||||
holdVotes++;
|
||||
break;
|
||||
}
|
||||
|
||||
totalConfidence += signal.Confidence;
|
||||
}
|
||||
}
|
||||
|
||||
// Update engine status
|
||||
if (_engineStatuses.TryGetValue(symbol, out var status))
|
||||
{
|
||||
status.RecentSignals = signals;
|
||||
status.LastSignalTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Aggregate decision
|
||||
var decision = MakeDecision(symbol, signals, buyVotes, sellVotes, holdVotes, totalConfidence);
|
||||
|
||||
if (status != null)
|
||||
{
|
||||
status.LastDecision = decision;
|
||||
}
|
||||
|
||||
OnDecisionMade?.Invoke(symbol, decision);
|
||||
return decision;
|
||||
}
|
||||
|
||||
private TradingDecision MakeDecision(string symbol, List<StrategySignal> signals, int buyVotes, int sellVotes, int holdVotes, decimal totalConfidence)
|
||||
{
|
||||
var totalVotes = buyVotes + sellVotes + holdVotes;
|
||||
if (totalVotes == 0)
|
||||
{
|
||||
return new TradingDecision
|
||||
{
|
||||
Symbol = symbol,
|
||||
Decision = SignalType.Hold,
|
||||
Confidence = 0,
|
||||
Reason = "Nessuna strategia attiva"
|
||||
};
|
||||
}
|
||||
|
||||
var avgConfidence = totalConfidence / totalVotes;
|
||||
SignalType finalDecision;
|
||||
string reason;
|
||||
List<string> supporting = new();
|
||||
List<string> opposing = new();
|
||||
|
||||
// Decision logic: majority voting with confidence threshold
|
||||
if (buyVotes > sellVotes && buyVotes >= totalVotes * 0.6m)
|
||||
{
|
||||
finalDecision = SignalType.Buy;
|
||||
reason = $"{buyVotes}/{totalVotes} strategie suggeriscono acquisto";
|
||||
supporting = signals.Where(s => s.Signal.Type == SignalType.Buy).Select(s => s.StrategyName).ToList();
|
||||
opposing = signals.Where(s => s.Signal.Type != SignalType.Buy).Select(s => s.StrategyName).ToList();
|
||||
}
|
||||
else if (sellVotes > buyVotes && sellVotes >= totalVotes * 0.6m)
|
||||
{
|
||||
finalDecision = SignalType.Sell;
|
||||
reason = $"{sellVotes}/{totalVotes} strategie suggeriscono vendita";
|
||||
supporting = signals.Where(s => s.Signal.Type == SignalType.Sell).Select(s => s.StrategyName).ToList();
|
||||
opposing = signals.Where(s => s.Signal.Type != SignalType.Sell).Select(s => s.StrategyName).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
finalDecision = SignalType.Hold;
|
||||
reason = "Segnali contrastanti - attendi conferma";
|
||||
supporting = signals.Where(s => s.Signal.Type == SignalType.Hold).Select(s => s.StrategyName).ToList();
|
||||
}
|
||||
|
||||
return new TradingDecision
|
||||
{
|
||||
Symbol = symbol,
|
||||
Decision = finalDecision,
|
||||
Confidence = avgConfidence,
|
||||
Reason = reason,
|
||||
BuyVotes = buyVotes,
|
||||
SellVotes = sellVotes,
|
||||
HoldVotes = holdVotes,
|
||||
SupportingStrategies = supporting,
|
||||
OpposingStrategies = opposing
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get trading engine status for asset
|
||||
/// </summary>
|
||||
public TradingEngineStatus? GetEngineStatus(string symbol)
|
||||
{
|
||||
_engineStatuses.TryGetValue(symbol, out var status);
|
||||
return status;
|
||||
}
|
||||
|
||||
private void SaveMappings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_configPath);
|
||||
if (directory != null && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(_assetMappings, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
File.WriteAllText(_configPath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error saving strategy mappings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadMappings()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_configPath))
|
||||
{
|
||||
var json = File.ReadAllText(_configPath);
|
||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, AssetStrategyMapping>>(json);
|
||||
|
||||
if (loaded != null)
|
||||
{
|
||||
foreach (var kvp in loaded)
|
||||
{
|
||||
_assetMappings[kvp.Key] = kvp.Value;
|
||||
|
||||
// Initialize engine status
|
||||
_engineStatuses[kvp.Key] = new TradingEngineStatus
|
||||
{
|
||||
Symbol = kvp.Key,
|
||||
IsRunning = kvp.Value.IsActive,
|
||||
ActiveStrategies = kvp.Value.StrategyIds.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading strategy mappings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user