Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-08-05 10:05:20 +02:00
parent 61f1e59964
commit f96ed670ca
239 changed files with 23858 additions and 730 deletions
+101
View File
@@ -0,0 +1,101 @@
using System.Text.Json;
using TradingBot.Models;
namespace TradingBot.Services;
public class CoinGeckoMarketDataService : IMarketDataService
{
private readonly HttpClient _httpClient;
private readonly Dictionary<string, string> _symbolToId = new()
{
{ "BTC", "bitcoin" },
{ "ETH", "ethereum" },
{ "BNB", "binancecoin" },
{ "XRP", "ripple" },
{ "ADA", "cardano" },
{ "SOL", "solana" },
{ "DOT", "polkadot" }
};
public CoinGeckoMarketDataService(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://api.coingecko.com/api/v3/");
_httpClient.DefaultRequestHeaders.Add("User-Agent", "NovaTrader-Bot");
}
public async Task<List<MarketPrice>> GetMarketPricesAsync(List<string> symbols)
{
var prices = new List<MarketPrice>();
// Convert symbols to CoinGecko IDs
var ids = string.Join(",", symbols.Select(s => _symbolToId.GetValueOrDefault(s.ToUpper(), s.ToLower())));
try
{
// CoinGecko API: /simple/price endpoint
var response = await _httpClient.GetAsync(
$"simple/price?ids={ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true");
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
if (data != null)
{
foreach (var symbol in symbols)
{
var coinId = _symbolToId.GetValueOrDefault(symbol.ToUpper(), symbol.ToLower());
if (data.TryGetValue(coinId, out var coinData))
{
var price = new MarketPrice
{
Symbol = symbol.ToUpper(),
Price = coinData.GetProperty("usd").GetDecimal(),
Timestamp = DateTime.UtcNow
};
// Safely get optional properties
if (coinData.TryGetProperty("usd_24h_change", out var changeElement))
{
price.Change24h = changeElement.GetDecimal();
}
if (coinData.TryGetProperty("usd_24h_vol", out var volumeElement))
{
price.Volume24h = volumeElement.GetDecimal();
}
prices.Add(price);
}
}
}
}
else
{
Console.WriteLine($"CoinGecko API error: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}");
}
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Network error fetching market data: {ex.Message}");
}
catch (JsonException ex)
{
Console.WriteLine($"JSON parsing error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error fetching market data: {ex.Message}");
}
return prices;
}
public async Task<MarketPrice?> GetPriceAsync(string symbol)
{
var prices = await GetMarketPricesAsync(new List<string> { symbol });
return prices.FirstOrDefault();
}
}
+9
View File
@@ -0,0 +1,9 @@
using TradingBot.Models;
namespace TradingBot.Services;
public interface IMarketDataService
{
Task<List<MarketPrice>> GetMarketPricesAsync(List<string> symbols);
Task<MarketPrice?> GetPriceAsync(string symbol);
}
+9
View File
@@ -0,0 +1,9 @@
using TradingBot.Models;
namespace TradingBot.Services;
public interface ITradingStrategy
{
string Name { get; }
Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> historicalPrices);
}
+346
View 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();
}
+122
View File
@@ -0,0 +1,122 @@
using TradingBot.Models;
using System.Collections.Concurrent;
namespace TradingBot.Services;
/// <summary>
/// Centralized logging service for application events
/// </summary>
public class LoggingService
{
private readonly ConcurrentQueue<LogEntry> _logs = new();
private const int MaxLogEntries = 500;
public event Action? OnLogAdded;
/// <summary>
/// Get all log entries
/// </summary>
public IReadOnlyList<LogEntry> GetLogs()
{
return _logs.ToList().AsReadOnly();
}
/// <summary>
/// Add a debug log entry
/// </summary>
public void LogDebug(string category, string message, string? details = null)
{
AddLog(Models.LogLevel.Debug, category, message, details);
}
/// <summary>
/// Add an info log entry
/// </summary>
public void LogInfo(string category, string message, string? details = null, string? symbol = null)
{
AddLog(Models.LogLevel.Info, category, message, details, symbol);
}
/// <summary>
/// Add a warning log entry
/// </summary>
public void LogWarning(string category, string message, string? details = null, string? symbol = null)
{
AddLog(Models.LogLevel.Warning, category, message, details, symbol);
}
/// <summary>
/// Add an error log entry
/// </summary>
public void LogError(string category, string message, string? details = null, string? symbol = null)
{
AddLog(Models.LogLevel.Error, category, message, details, symbol);
}
/// <summary>
/// Add a trade log entry
/// </summary>
public void LogTrade(string symbol, string message, string? details = null)
{
AddLog(Models.LogLevel.Trade, "Trading", message, details, symbol);
}
/// <summary>
/// Clear all logs
/// </summary>
public void ClearLogs()
{
_logs.Clear();
OnLogAdded?.Invoke();
}
/// <summary>
/// Get logs filtered by level
/// </summary>
public IReadOnlyList<LogEntry> GetLogsByLevel(Models.LogLevel level)
{
return _logs.Where(l => l.Level == level).ToList().AsReadOnly();
}
/// <summary>
/// Get logs filtered by category
/// </summary>
public IReadOnlyList<LogEntry> GetLogsByCategory(string category)
{
return _logs.Where(l => l.Category.Equals(category, StringComparison.OrdinalIgnoreCase))
.ToList()
.AsReadOnly();
}
/// <summary>
/// Get logs filtered by symbol
/// </summary>
public IReadOnlyList<LogEntry> GetLogsBySymbol(string symbol)
{
return _logs.Where(l => l.Symbol != null && l.Symbol.Equals(symbol, StringComparison.OrdinalIgnoreCase))
.ToList()
.AsReadOnly();
}
private void AddLog(Models.LogLevel level, string category, string message, string? details = null, string? symbol = null)
{
var logEntry = new LogEntry
{
Level = level,
Category = category,
Message = message,
Details = details,
Symbol = symbol
};
_logs.Enqueue(logEntry);
// Maintain max size
while (_logs.Count > MaxLogEntries)
{
_logs.TryDequeue(out _);
}
OnLogAdded?.Invoke();
}
}
+96
View File
@@ -0,0 +1,96 @@
using System.Text.Json;
using TradingBot.Models;
namespace TradingBot.Services;
public class SettingsService
{
private const string SettingsFileName = "appsettings.json";
private AppSettings _settings;
private readonly string _settingsPath;
public event Action? OnSettingsChanged;
public SettingsService()
{
_settingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"TradingBot",
SettingsFileName
);
_settings = LoadSettings();
}
public AppSettings GetSettings()
{
return _settings;
}
public void UpdateSettings(AppSettings settings)
{
_settings = settings;
SaveSettings();
OnSettingsChanged?.Invoke();
}
public void UpdateSetting<T>(string propertyName, T value)
{
var property = typeof(AppSettings).GetProperty(propertyName);
if (property != null && property.CanWrite)
{
property.SetValue(_settings, value);
SaveSettings();
OnSettingsChanged?.Invoke();
}
}
private AppSettings LoadSettings()
{
try
{
if (File.Exists(_settingsPath))
{
var json = File.ReadAllText(_settingsPath);
var settings = JsonSerializer.Deserialize<AppSettings>(json);
return settings ?? new AppSettings();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading settings: {ex.Message}");
}
return new AppSettings();
}
private void SaveSettings()
{
try
{
var directory = Path.GetDirectoryName(_settingsPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(_settings, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_settingsPath, json);
}
catch (Exception ex)
{
Console.WriteLine($"Error saving settings: {ex.Message}");
}
}
public void ResetToDefaults()
{
_settings = new AppSettings();
SaveSettings();
OnSettingsChanged?.Invoke();
}
}
@@ -0,0 +1,87 @@
using TradingBot.Models;
namespace TradingBot.Services;
public class SimpleMovingAverageStrategy : ITradingStrategy
{
private readonly int _shortPeriod = 5;
private readonly int _longPeriod = 10;
public string Name => "Simple Moving Average (SMA)";
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> historicalPrices)
{
// Filtra null e valori invalidi prima di usare la lista
if (historicalPrices == null || historicalPrices.Count < _longPeriod)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Price = historicalPrices?.LastOrDefault()?.Price ?? 0,
Reason = "Dati insufficienti per l'analisi",
Timestamp = DateTime.UtcNow
});
}
// Filtra oggetti null e ordina
var recentPrices = historicalPrices
.Where(p => p != null && p.Price > 0)
.OrderByDescending(p => p.Timestamp)
.Take(_longPeriod)
.ToList();
// Verifica ancora la count dopo il filtro
if (recentPrices.Count < _longPeriod)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Price = recentPrices.LastOrDefault()?.Price ?? 0,
Reason = "Dati insufficienti per l'analisi dopo il filtro",
Timestamp = DateTime.UtcNow
});
}
var shortSMA = recentPrices.Take(_shortPeriod).Average(p => p.Price);
var longSMA = recentPrices.Average(p => p.Price);
var currentPrice = recentPrices.First().Price;
// Strategia: Compra quando la SMA breve incrocia sopra la SMA lunga
// Vendi quando la SMA breve incrocia sotto la SMA lunga
if (shortSMA > longSMA * 1.02m) // 2% sopra
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Price = currentPrice,
Reason = $"SMA breve ({shortSMA:F2}) > SMA lunga ({longSMA:F2}) - Trend rialzista",
Timestamp = DateTime.UtcNow
});
}
else if (shortSMA < longSMA * 0.98m) // 2% sotto
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Price = currentPrice,
Reason = $"SMA breve ({shortSMA:F2}) < SMA lunga ({longSMA:F2}) - Trend ribassista",
Timestamp = DateTime.UtcNow
});
}
else
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Price = currentPrice,
Reason = $"SMA breve ({shortSMA:F2}) ? SMA lunga ({longSMA:F2}) - Nessun segnale chiaro",
Timestamp = DateTime.UtcNow
});
}
}
}
+209
View File
@@ -0,0 +1,209 @@
using TradingBot.Models;
namespace TradingBot.Services;
public class SimulatedMarketDataService : IMarketDataService
{
private readonly Dictionary<string, SimulatedAsset> _assets = new();
private readonly Random _random = new();
private readonly Timer _updateTimer;
private readonly object _lock = new();
public event Action? OnPriceUpdated;
public SimulatedMarketDataService()
{
InitializeAssets();
_updateTimer = new Timer(UpdatePrices, null, TimeSpan.Zero, TimeSpan.FromSeconds(2));
}
private void InitializeAssets()
{
var assets = new[]
{
new { Symbol = "BTC", Name = "Bitcoin", BasePrice = 45000m, Volatility = 0.02m, TrendBias = 0.0002m },
new { Symbol = "ETH", Name = "Ethereum", BasePrice = 2500m, Volatility = 0.025m, TrendBias = 0.0003m },
new { Symbol = "BNB", Name = "Binance Coin", BasePrice = 350m, Volatility = 0.03m, TrendBias = 0.0001m },
new { Symbol = "SOL", Name = "Solana", BasePrice = 100m, Volatility = 0.035m, TrendBias = 0.0004m },
new { Symbol = "ADA", Name = "Cardano", BasePrice = 0.45m, Volatility = 0.028m, TrendBias = 0.0002m },
new { Symbol = "XRP", Name = "Ripple", BasePrice = 0.65m, Volatility = 0.032m, TrendBias = 0.0001m },
new { Symbol = "DOT", Name = "Polkadot", BasePrice = 6.5m, Volatility = 0.03m, TrendBias = 0.0003m },
new { Symbol = "AVAX", Name = "Avalanche", BasePrice = 35m, Volatility = 0.038m, TrendBias = 0.0005m },
new { Symbol = "MATIC", Name = "Polygon", BasePrice = 0.85m, Volatility = 0.033m, TrendBias = 0.0002m },
new { Symbol = "LINK", Name = "Chainlink", BasePrice = 15m, Volatility = 0.029m, TrendBias = 0.0003m },
new { Symbol = "UNI", Name = "Uniswap", BasePrice = 6.5m, Volatility = 0.031m, TrendBias = 0.0001m },
new { Symbol = "ATOM", Name = "Cosmos", BasePrice = 10m, Volatility = 0.03m, TrendBias = 0.0004m },
new { Symbol = "LTC", Name = "Litecoin", BasePrice = 75m, Volatility = 0.025m, TrendBias = 0.0001m },
new { Symbol = "ALGO", Name = "Algorand", BasePrice = 0.25m, Volatility = 0.032m, TrendBias = 0.0003m },
new { Symbol = "VET", Name = "VeChain", BasePrice = 0.03m, Volatility = 0.035m, TrendBias = 0.0002m }
};
foreach (var asset in assets)
{
_assets[asset.Symbol] = new SimulatedAsset
{
Symbol = asset.Symbol,
Name = asset.Name,
CurrentPrice = asset.BasePrice,
BasePrice = asset.BasePrice,
Volatility = asset.Volatility,
TrendBias = asset.TrendBias,
LastUpdate = DateTime.UtcNow
};
}
}
private void UpdatePrices(object? state)
{
lock (_lock)
{
var now = DateTime.UtcNow;
foreach (var asset in _assets.Values)
{
// Calculate time-based factors
var timeSinceStart = (now - asset.LastUpdate).TotalSeconds;
// Generate random walk with trend
var randomChange = (_random.NextDouble() - 0.5) * 2 * (double)asset.Volatility;
var trendComponent = (double)asset.TrendBias;
// Add market cycles (sine wave for realistic market behavior)
var cycleComponent = Math.Sin((double)asset.PriceUpdateCount / 100.0) * 0.001;
// Combine all factors
var totalChange = randomChange + trendComponent + cycleComponent;
// Update price
var newPrice = asset.CurrentPrice * (1 + (decimal)totalChange);
// Keep price within reasonable bounds (50% to 200% of base price)
newPrice = Math.Max(asset.BasePrice * 0.5m, Math.Min(asset.BasePrice * 2.0m, newPrice));
// Calculate change and volume
var priceChange = newPrice - asset.CurrentPrice;
var changePercentage = asset.CurrentPrice > 0 ? (priceChange / asset.CurrentPrice) * 100 : 0;
// Simulate volume based on volatility and price change
var baseVolume = asset.BasePrice * 1000000m;
var volumeVariation = (decimal)(_random.NextDouble() * 0.5 + 0.75); // 75% to 125%
var volumeFromVolatility = Math.Abs(changePercentage) * 100000m;
asset.CurrentPrice = newPrice;
asset.Change24h = changePercentage;
asset.Volume24h = (baseVolume + volumeFromVolatility) * volumeVariation;
asset.LastUpdate = now;
asset.PriceUpdateCount++;
// Add to history
asset.PriceHistory.Add(new MarketPrice
{
Symbol = asset.Symbol,
Price = newPrice,
Change24h = changePercentage,
Volume24h = asset.Volume24h,
Timestamp = now
});
// Keep history limited to last 500 points
if (asset.PriceHistory.Count > 500)
{
asset.PriceHistory.RemoveAt(0);
}
}
OnPriceUpdated?.Invoke();
}
}
public Task<List<MarketPrice>> GetMarketPricesAsync(List<string> symbols)
{
lock (_lock)
{
var prices = new List<MarketPrice>();
foreach (var symbol in symbols)
{
if (_assets.TryGetValue(symbol, out var asset))
{
prices.Add(new MarketPrice
{
Symbol = asset.Symbol,
Price = asset.CurrentPrice,
Change24h = asset.Change24h,
Volume24h = asset.Volume24h,
Timestamp = asset.LastUpdate
});
}
}
return Task.FromResult(prices);
}
}
public Task<MarketPrice?> GetPriceAsync(string symbol)
{
lock (_lock)
{
if (_assets.TryGetValue(symbol, out var asset))
{
return Task.FromResult<MarketPrice?>(new MarketPrice
{
Symbol = asset.Symbol,
Price = asset.CurrentPrice,
Change24h = asset.Change24h,
Volume24h = asset.Volume24h,
Timestamp = asset.LastUpdate
});
}
return Task.FromResult<MarketPrice?>(null);
}
}
public List<MarketPrice> GetPriceHistory(string symbol, int count = 100)
{
lock (_lock)
{
if (_assets.TryGetValue(symbol, out var asset))
{
return asset.PriceHistory
.Skip(Math.Max(0, asset.PriceHistory.Count - count))
.ToList();
}
return new List<MarketPrice>();
}
}
public List<string> GetAvailableSymbols()
{
lock (_lock)
{
return _assets.Keys.OrderBy(s => s).ToList();
}
}
public Dictionary<string, string> GetAssetNames()
{
lock (_lock)
{
return _assets.ToDictionary(a => a.Key, a => a.Value.Name);
}
}
private class SimulatedAsset
{
public string Symbol { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public decimal CurrentPrice { get; set; }
public decimal BasePrice { get; set; }
public decimal Change24h { get; set; }
public decimal Volume24h { get; set; }
public decimal Volatility { get; set; }
public decimal TrendBias { get; set; }
public DateTime LastUpdate { get; set; }
public int PriceUpdateCount { get; set; }
public List<MarketPrice> PriceHistory { get; set; } = new();
}
}
+91
View File
@@ -0,0 +1,91 @@
namespace TradingBot.Services;
public static class TechnicalAnalysis
{
public static decimal CalculateEMA(List<decimal> prices, int period)
{
if (prices.Count == 0) return 0;
decimal k = 2m / (period + 1);
decimal ema = prices[0];
for (int i = 1; i < prices.Count; i++)
{
ema = prices[i] * k + ema * (1 - k);
}
return ema;
}
public static List<decimal> CalculateEMAArray(List<decimal> prices, int period)
{
if (prices.Count == 0) return new List<decimal>();
decimal k = 2m / (period + 1);
var emaArray = new List<decimal> { prices[0] };
for (int i = 1; i < prices.Count; i++)
{
emaArray.Add(prices[i] * k + emaArray[i - 1] * (1 - k));
}
return emaArray;
}
public static decimal CalculateRSI(List<decimal> prices, int period = 14)
{
if (prices.Count < period + 1) return 50;
decimal gains = 0;
decimal losses = 0;
for (int i = prices.Count - period; i < prices.Count; i++)
{
decimal diff = prices[i] - prices[i - 1];
if (diff >= 0)
gains += diff;
else
losses -= diff;
}
decimal avgGain = gains / period;
decimal avgLoss = losses / period;
if (avgLoss == 0) return 100;
decimal rs = avgGain / avgLoss;
return 100 - (100 / (1 + rs));
}
public static (decimal macd, decimal signal, decimal histogram) CalculateMACD(List<decimal> prices)
{
if (prices.Count < 26) return (0, 0, 0);
var ema12Array = CalculateEMAArray(prices, 12);
var ema26Array = CalculateEMAArray(prices, 26);
var macdLine = ema12Array[^1] - ema26Array[^1];
var signalLine = macdLine * 0.9m; // Simplified signal
var histogram = macdLine - signalLine;
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);
}
}
+164
View File
@@ -0,0 +1,164 @@
using System.Text.Json;
using TradingBot.Models;
namespace TradingBot.Services;
/// <summary>
/// Service for persisting trade history and active positions to disk
/// </summary>
public class TradeHistoryService
{
private readonly string _dataDirectory;
private readonly string _tradesFilePath;
private readonly string _activePositionsFilePath;
private readonly ILogger<TradeHistoryService> _logger;
private readonly JsonSerializerOptions _jsonOptions;
public TradeHistoryService(ILogger<TradeHistoryService> logger)
{
_logger = logger;
_dataDirectory = Path.Combine(Directory.GetCurrentDirectory(), "data");
_tradesFilePath = Path.Combine(_dataDirectory, "trade-history.json");
_activePositionsFilePath = Path.Combine(_dataDirectory, "active-positions.json");
_jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
EnsureDataDirectoryExists();
}
private void EnsureDataDirectoryExists()
{
if (!Directory.Exists(_dataDirectory))
{
Directory.CreateDirectory(_dataDirectory);
_logger.LogInformation("Created data directory: {Directory}", _dataDirectory);
}
}
/// <summary>
/// Save complete trade history to disk
/// </summary>
public async Task SaveTradeHistoryAsync(List<Trade> trades)
{
try
{
var json = JsonSerializer.Serialize(trades, _jsonOptions);
await File.WriteAllTextAsync(_tradesFilePath, json);
_logger.LogInformation("Saved {Count} trades to history", trades.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save trade history");
}
}
/// <summary>
/// Load trade history from disk
/// </summary>
public async Task<List<Trade>> LoadTradeHistoryAsync()
{
try
{
if (!File.Exists(_tradesFilePath))
{
_logger.LogInformation("No trade history file found, starting fresh");
return new List<Trade>();
}
var json = await File.ReadAllTextAsync(_tradesFilePath);
var trades = JsonSerializer.Deserialize<List<Trade>>(json, _jsonOptions);
_logger.LogInformation("Loaded {Count} trades from history", trades?.Count ?? 0);
return trades ?? new List<Trade>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load trade history, starting fresh");
return new List<Trade>();
}
}
/// <summary>
/// Save active positions (open trades) to disk
/// </summary>
public async Task SaveActivePositionsAsync(Dictionary<string, Trade> activePositions)
{
try
{
var json = JsonSerializer.Serialize(activePositions, _jsonOptions);
await File.WriteAllTextAsync(_activePositionsFilePath, json);
_logger.LogInformation("Saved {Count} active positions", activePositions.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save active positions");
}
}
/// <summary>
/// Load active positions from disk
/// </summary>
public async Task<Dictionary<string, Trade>> LoadActivePositionsAsync()
{
try
{
if (!File.Exists(_activePositionsFilePath))
{
_logger.LogInformation("No active positions file found");
return new Dictionary<string, Trade>();
}
var json = await File.ReadAllTextAsync(_activePositionsFilePath);
var positions = JsonSerializer.Deserialize<Dictionary<string, Trade>>(json, _jsonOptions);
_logger.LogInformation("Loaded {Count} active positions", positions?.Count ?? 0);
return positions ?? new Dictionary<string, Trade>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load active positions");
return new Dictionary<string, Trade>();
}
}
/// <summary>
/// Clear all persisted data
/// </summary>
public void ClearAll()
{
try
{
if (File.Exists(_tradesFilePath))
File.Delete(_tradesFilePath);
if (File.Exists(_activePositionsFilePath))
File.Delete(_activePositionsFilePath);
_logger.LogInformation("Cleared all persisted trade data");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to clear persisted data");
}
}
/// <summary>
/// Get total file size of persisted data
/// </summary>
public long GetDataSize()
{
long size = 0;
if (File.Exists(_tradesFilePath))
size += new FileInfo(_tradesFilePath).Length;
if (File.Exists(_activePositionsFilePath))
size += new FileInfo(_activePositionsFilePath).Length;
return size;
}
}
@@ -0,0 +1,58 @@
namespace TradingBot.Services;
/// <summary>
/// Background service for automatic data persistence on application shutdown
/// </summary>
public class TradingBotBackgroundService : BackgroundService
{
private readonly TradingBotService _tradingBotService;
private readonly ILogger<TradingBotBackgroundService> _logger;
private readonly IHostApplicationLifetime _lifetime;
public TradingBotBackgroundService(
TradingBotService tradingBotService,
ILogger<TradingBotBackgroundService> logger,
IHostApplicationLifetime lifetime)
{
_tradingBotService = tradingBotService;
_logger = logger;
_lifetime = lifetime;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("TradingBot Background Service started");
// Register shutdown handler
_lifetime.ApplicationStopping.Register(OnShutdown);
// Keep service running
await Task.Delay(Timeout.Infinite, stoppingToken);
}
private void OnShutdown()
{
_logger.LogInformation("Application shutdown detected, saving trade data...");
try
{
// Stop bot if running
if (_tradingBotService.Status.IsRunning)
{
_tradingBotService.Stop();
}
_logger.LogInformation("Trade data saved successfully on shutdown");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving data on shutdown");
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("TradingBot Background Service stopping");
await base.StopAsync(cancellationToken);
}
}
+796
View File
@@ -0,0 +1,796 @@
using TradingBot.Models;
namespace TradingBot.Services;
public class TradingBotService
{
private readonly IMarketDataService _marketDataService;
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();
private readonly Dictionary<string, List<MarketPrice>> _priceHistory = new();
private readonly Dictionary<string, TechnicalIndicators> _indicators = new();
private readonly Dictionary<string, Trade> _activePositions = new();
private Timer? _timer;
private Timer? _persistenceTimer;
public BotStatus Status { get; private set; } = new();
public IReadOnlyList<Trade> Trades => _trades.AsReadOnly();
public IReadOnlyDictionary<string, AssetConfiguration> AssetConfigurations => _assetConfigs;
public IReadOnlyDictionary<string, AssetStatistics> AssetStatistics => _assetStats;
public IReadOnlyDictionary<string, Trade> ActivePositions => _activePositions;
public event Action? OnStatusChanged;
public event Action<TradingSignal>? OnSignalGenerated;
public event Action<Trade>? OnTradeExecuted;
public event Action<string, TechnicalIndicators>? OnIndicatorsUpdated;
public event Action<string, MarketPrice>? OnPriceUpdated;
public event Action? OnStatisticsUpdated;
public TradingBotService(
IMarketDataService marketDataService,
ITradingStrategy strategy,
TradeHistoryService historyService,
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
if (_marketDataService is SimulatedMarketDataService simService)
{
simService.OnPriceUpdated += HandleSimulatedPriceUpdate;
}
InitializeDefaultAssets();
// Load persisted data
_ = LoadPersistedDataAsync();
_loggingService.LogInfo("System", "TradingBot Service initialized");
}
private async Task LoadPersistedDataAsync()
{
try
{
// Load trade history
var trades = await _historyService.LoadTradeHistoryAsync();
_trades.AddRange(trades);
// Load active positions
var positions = await _historyService.LoadActivePositionsAsync();
foreach (var kvp in positions)
{
_activePositions[kvp.Key] = kvp.Value;
}
// Restore asset configurations from active positions
RestoreAssetConfigurationsFromTrades();
OnStatusChanged?.Invoke();
}
catch (Exception ex)
{
Console.WriteLine($"Error loading persisted data: {ex.Message}");
}
}
private void RestoreAssetConfigurationsFromTrades()
{
foreach (var position in _activePositions.Values)
{
if (_assetConfigs.TryGetValue(position.Symbol, out var config))
{
if (position.Type == TradeType.Buy)
{
config.CurrentHoldings += position.Amount;
config.AverageEntryPrice = position.Price;
}
}
}
}
private void InitializeDefaultAssets()
{
// Get available symbols from SimulatedMarketDataService
var availableSymbols = _marketDataService is SimulatedMarketDataService simService
? simService.GetAvailableSymbols()
: new List<string> { "BTC", "ETH", "SOL", "ADA", "MATIC" };
var assetNames = _marketDataService is SimulatedMarketDataService simService2
? simService2.GetAssetNames()
: new Dictionary<string, string>
{
{ "BTC", "Bitcoin" },
{ "ETH", "Ethereum" },
{ "SOL", "Solana" },
{ "ADA", "Cardano" },
{ "MATIC", "Polygon" }
};
foreach (var symbol in availableSymbols)
{
_assetConfigs[symbol] = new AssetConfiguration
{
Symbol = symbol,
Name = assetNames.TryGetValue(symbol, out var name) ? name : symbol,
IsEnabled = true,
InitialBalance = 1000m,
CurrentBalance = 1000m
};
_assetStats[symbol] = new AssetStatistics
{
Symbol = symbol,
Name = assetNames.TryGetValue(symbol, out var name2) ? name2 : symbol
};
}
}
public void UpdateAssetConfiguration(string symbol, AssetConfiguration config)
{
_assetConfigs[symbol] = config;
OnStatusChanged?.Invoke();
}
public void ToggleAsset(string symbol, bool enabled)
{
if (_assetConfigs.TryGetValue(symbol, out var config))
{
config.IsEnabled = enabled;
OnStatusChanged?.Invoke();
}
}
public void AddAsset(string symbol, string name)
{
if (!_assetConfigs.ContainsKey(symbol))
{
_assetConfigs[symbol] = new AssetConfiguration
{
Symbol = symbol,
Name = name,
IsEnabled = false,
InitialBalance = 1000m,
CurrentBalance = 1000m
};
_assetStats[symbol] = new AssetStatistics
{
Symbol = symbol,
Name = name
};
OnStatusChanged?.Invoke();
}
}
public void Start()
{
if (Status.IsRunning) return;
Status.IsRunning = true;
Status.StartedAt = DateTime.UtcNow;
_loggingService.LogInfo("Bot", "Trading Bot started", $"Strategy: {_strategy.Name}");
// Reset daily trade counts
foreach (var config in _assetConfigs.Values)
{
if (config.DailyTradeCountReset.Date < DateTime.UtcNow.Date)
{
config.DailyTradeCount = 0;
config.DailyTradeCountReset = DateTime.UtcNow.Date;
}
}
// Start update timer (every 3 seconds for simulation)
_timer = new Timer(async _ => await UpdateAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(3));
// Start persistence timer (save every 30 seconds)
_persistenceTimer = new Timer(
async _ => await SaveDataAsync(),
null,
TimeSpan.FromSeconds(30),
TimeSpan.FromSeconds(30));
OnStatusChanged?.Invoke();
}
public async void Stop()
{
if (!Status.IsRunning) return;
Status.IsRunning = false;
_timer?.Dispose();
_timer = null;
_persistenceTimer?.Dispose();
_persistenceTimer = null;
_loggingService.LogInfo("Bot", "Trading Bot stopped", $"Total trades: {_trades.Count}");
// Save data on stop
await SaveDataAsync();
OnStatusChanged?.Invoke();
}
private async Task SaveDataAsync()
{
try
{
await _historyService.SaveTradeHistoryAsync(_trades);
await _historyService.SaveActivePositionsAsync(_activePositions);
}
catch (Exception ex)
{
Console.WriteLine($"Error saving data: {ex.Message}");
}
}
private void HandleSimulatedPriceUpdate()
{
if (Status.IsRunning)
{
_ = UpdateAsync();
}
}
private async Task UpdateAsync()
{
try
{
var enabledSymbols = _assetConfigs.Values
.Where(c => c != null && c.IsEnabled)
.Select(c => c.Symbol)
.Where(s => !string.IsNullOrWhiteSpace(s))
.ToList();
if (enabledSymbols.Count == 0) return;
var prices = await _marketDataService.GetMarketPricesAsync(enabledSymbols);
if (prices == null) return;
foreach (var price in prices)
{
if (price != null)
{
await ProcessAssetUpdate(price);
}
}
UpdateGlobalStatistics();
}
catch (Exception ex)
{
Console.WriteLine($"Error in UpdateAsync: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
}
private async Task ProcessAssetUpdate(MarketPrice price)
{
if (price == null || price.Price <= 0)
return;
if (!_assetConfigs.TryGetValue(price.Symbol, out var config) || !config.IsEnabled)
return;
// Update price history
if (!_priceHistory.ContainsKey(price.Symbol))
{
_priceHistory[price.Symbol] = new List<MarketPrice>();
}
_priceHistory[price.Symbol].Add(price);
if (_priceHistory[price.Symbol].Count > 200)
{
_priceHistory[price.Symbol].RemoveAt(0);
}
// Update statistics current price
if (_assetStats.TryGetValue(price.Symbol, out var stats))
{
stats.CurrentPrice = price.Price;
}
OnPriceUpdated?.Invoke(price.Symbol, price);
// Calculate indicators if enough data
if (_priceHistory[price.Symbol].Count >= 26)
{
UpdateIndicators(price.Symbol);
// Generate trading signal
var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]);
if (signal != null)
{
OnSignalGenerated?.Invoke(signal);
// Execute trades based on strategy and configuration
await EvaluateAndExecuteTrade(price.Symbol, signal, price, config);
}
}
}
private async Task EvaluateAndExecuteTrade(string symbol, TradingSignal signal, MarketPrice price, AssetConfiguration config)
{
if (!_indicators.TryGetValue(symbol, out var indicators))
return;
// Check daily trade limit
if (config.DailyTradeCount >= config.MaxDailyTrades)
return;
// Check if enough time has passed since last trade (min 10 seconds)
if (config.LastTradeTime.HasValue &&
(DateTime.UtcNow - config.LastTradeTime.Value).TotalSeconds < 10)
return;
// Buy logic
if (signal.Type == SignalType.Buy &&
indicators.RSI < 40 &&
indicators.Histogram > 0 &&
config.CurrentBalance >= config.MinTradeAmount)
{
var tradeAmount = Math.Min(
Math.Min(config.CurrentBalance * 0.3m, config.MaxTradeAmount),
config.MaxPositionSize - (config.CurrentHoldings * price.Price)
);
if (tradeAmount >= config.MinTradeAmount)
{
await ExecuteBuyAsync(symbol, price.Price, tradeAmount, config);
}
}
// Sell logic
else if (signal.Type == SignalType.Sell &&
indicators.RSI > 60 &&
indicators.Histogram < 0 &&
config.CurrentHoldings > 0)
{
var profitPercentage = config.AverageEntryPrice > 0
? ((price.Price - config.AverageEntryPrice) / config.AverageEntryPrice) * 100
: 0;
// Sell if profit target reached or stop loss triggered
if (profitPercentage >= config.TakeProfitPercentage ||
profitPercentage <= -config.StopLossPercentage)
{
await ExecuteSellAsync(symbol, price.Price, config.CurrentHoldings, config);
}
}
}
private async Task ExecuteBuyAsync(string symbol, decimal price, decimal amountUSD, AssetConfiguration config)
{
var amount = amountUSD / price;
// Update config
var previousHoldings = config.CurrentHoldings;
config.CurrentHoldings += amount;
config.CurrentBalance -= amountUSD;
config.AverageEntryPrice = previousHoldings > 0
? ((config.AverageEntryPrice * previousHoldings) + (price * amount)) / config.CurrentHoldings
: price;
config.LastTradeTime = DateTime.UtcNow;
config.DailyTradeCount++;
var trade = new Trade
{
Symbol = symbol,
Type = TradeType.Buy,
Price = price,
Amount = amount,
Timestamp = DateTime.UtcNow,
Strategy = _strategy.Name,
IsBot = true
};
_trades.Add(trade);
_activePositions[symbol] = trade;
UpdateAssetStatistics(symbol, trade);
Status.TradesExecuted++;
_loggingService.LogTrade(
symbol,
$"BUY {amount:F6} {symbol} @ ${price:N2}",
$"Value: ${amountUSD:N2} | Balance: ${config.CurrentBalance:N2}");
OnTradeExecuted?.Invoke(trade);
OnStatusChanged?.Invoke();
// Save immediately after trade
await SaveDataAsync();
}
private async Task ExecuteSellAsync(string symbol, decimal price, decimal amount, AssetConfiguration config)
{
var amountUSD = amount * price;
var profit = (price - config.AverageEntryPrice) * amount;
// Update config
config.CurrentHoldings = 0;
config.CurrentBalance += amountUSD;
config.LastTradeTime = DateTime.UtcNow;
config.DailyTradeCount++;
var trade = new Trade
{
Symbol = symbol,
Type = TradeType.Sell,
Price = price,
Amount = amount,
Timestamp = DateTime.UtcNow,
Strategy = _strategy.Name,
IsBot = true
};
_trades.Add(trade);
_activePositions.Remove(symbol);
UpdateAssetStatistics(symbol, trade, profit);
Status.TradesExecuted++;
_loggingService.LogTrade(
symbol,
$"SELL {amount:F6} {symbol} @ ${price:N2}",
$"Value: ${amountUSD:N2} | Profit: ${profit:N2} | Balance: ${config.CurrentBalance:N2}");
OnTradeExecuted?.Invoke(trade);
OnStatusChanged?.Invoke();
// Save immediately after trade
await SaveDataAsync();
}
private void UpdateIndicators(string symbol)
{
if (!_priceHistory.TryGetValue(symbol, out var history) || history == null || history.Count < 26)
return;
var prices = history
.Where(p => p != null && p.Price > 0)
.Select(p => p.Price)
.ToList();
if (prices.Count < 26)
return;
var rsi = TechnicalAnalysis.CalculateRSI(prices);
var (macd, signal, histogram) = TechnicalAnalysis.CalculateMACD(prices);
var indicators = new TechnicalIndicators
{
RSI = rsi,
MACD = macd,
Signal = signal,
Histogram = histogram,
EMA12 = TechnicalAnalysis.CalculateEMA(prices, 12),
EMA26 = TechnicalAnalysis.CalculateEMA(prices, 26)
};
_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))
return;
stats.TotalTrades++;
stats.RecentTrades.Insert(0, trade);
if (stats.RecentTrades.Count > 50)
stats.RecentTrades.RemoveAt(stats.RecentTrades.Count - 1);
if (!stats.FirstTradeTime.HasValue)
stats.FirstTradeTime = trade.Timestamp;
stats.LastTradeTime = trade.Timestamp;
if (realizedProfit.HasValue)
{
if (realizedProfit.Value > 0)
{
stats.WinningTrades++;
stats.TotalProfit += realizedProfit.Value;
stats.ConsecutiveWins++;
stats.ConsecutiveLosses = 0;
stats.MaxConsecutiveWins = Math.Max(stats.MaxConsecutiveWins, stats.ConsecutiveWins);
if (realizedProfit.Value > stats.LargestWin)
stats.LargestWin = realizedProfit.Value;
}
else if (realizedProfit.Value < 0)
{
stats.LosingTrades++;
stats.TotalLoss += Math.Abs(realizedProfit.Value);
stats.ConsecutiveLosses++;
stats.ConsecutiveWins = 0;
stats.MaxConsecutiveLosses = Math.Max(stats.MaxConsecutiveLosses, stats.ConsecutiveLosses);
if (Math.Abs(realizedProfit.Value) > stats.LargestLoss)
stats.LargestLoss = Math.Abs(realizedProfit.Value);
}
}
if (_assetConfigs.TryGetValue(symbol, out var config))
{
stats.TotalProfit = config.TotalProfit;
stats.ProfitPercentage = config.ProfitPercentage;
stats.CurrentPosition = config.CurrentHoldings;
stats.AverageEntryPrice = config.AverageEntryPrice;
}
OnStatisticsUpdated?.Invoke();
}
private void UpdateGlobalStatistics()
{
decimal totalProfit = 0;
int totalTrades = 0;
foreach (var config in _assetConfigs.Values.Where(c => c.IsEnabled))
{
totalProfit += config.TotalProfit;
}
totalTrades = _trades.Count;
Status.TotalProfit = totalProfit;
Status.TradesExecuted = totalTrades;
}
public PortfolioStatistics GetPortfolioStatistics()
{
var portfolio = new PortfolioStatistics
{
TotalAssets = _assetConfigs.Count,
ActiveAssets = _assetConfigs.Values.Count(c => c.IsEnabled),
TotalTrades = _trades.Count,
AssetStatistics = _assetStats.Values.ToList(),
StartDate = Status.StartedAt
};
portfolio.TotalBalance = _assetConfigs.Values.Sum(c =>
c.CurrentBalance + (c.CurrentHoldings * (_assetStats.TryGetValue(c.Symbol, out var s) ? s.CurrentPrice : 0)));
portfolio.InitialBalance = _assetConfigs.Values.Sum(c => c.InitialBalance);
if (_assetStats.Values.Any())
{
var winningTrades = _assetStats.Values.Sum(s => s.WinningTrades);
var totalTrades = _assetStats.Values.Sum(s => s.TotalTrades);
portfolio.WinRate = totalTrades > 0 ? (decimal)winningTrades / totalTrades * 100 : 0;
var bestAsset = _assetStats.Values.OrderByDescending(s => s.NetProfit).FirstOrDefault();
if (bestAsset != null)
{
portfolio.BestPerformingAssetSymbol = bestAsset.Symbol;
portfolio.BestPerformingAssetProfit = bestAsset.NetProfit;
}
var worstAsset = _assetStats.Values.OrderBy(s => s.NetProfit).FirstOrDefault();
if (worstAsset != null)
{
portfolio.WorstPerformingAssetSymbol = worstAsset.Symbol;
portfolio.WorstPerformingAssetProfit = worstAsset.NetProfit;
}
}
return portfolio;
}
public List<MarketPrice>? GetPriceHistory(string symbol)
{
return _priceHistory.TryGetValue(symbol, out var history) ? history : null;
}
public TechnicalIndicators? GetIndicators(string symbol)
{
return _indicators.TryGetValue(symbol, out var indicators) ? indicators : null;
}
public MarketPrice? GetLatestPrice(string symbol)
{
if (string.IsNullOrWhiteSpace(symbol))
return null;
var history = GetPriceHistory(symbol);
return history?.LastOrDefault();
}
public async Task ClearAllDataAsync()
{
_trades.Clear();
_activePositions.Clear();
_historyService.ClearAll();
foreach (var config in _assetConfigs.Values)
{
config.CurrentBalance = config.InitialBalance;
config.CurrentHoldings = 0;
config.AverageEntryPrice = 0;
config.DailyTradeCount = 0;
}
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
View 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
View 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}");
}
}
}