Aggiunta Bootstrap 5.3.3 (CSS, JS, RTL, mappe) al progetto
Sono stati aggiunti tutti i file principali di Bootstrap 5.3.3, inclusi CSS, JavaScript (bundle, ESM, UMD, minificati), versioni RTL, utility, reboot, griglia e relative mappe delle sorgenti. Questi file abilitano un sistema di design moderno, responsive e accessibile, con supporto per layout LTR e RTL, debugging avanzato tramite source map e tutte le funzionalità di Bootstrap per lo sviluppo dell’interfaccia utente. Nessuna modifica ai file esistenti.
This commit is contained in:
101
TradingBot/Services/CoinGeckoMarketDataService.cs
Normal file
101
TradingBot/Services/CoinGeckoMarketDataService.cs
Normal 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
TradingBot/Services/IMarketDataService.cs
Normal file
9
TradingBot/Services/IMarketDataService.cs
Normal 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
TradingBot/Services/ITradingStrategy.cs
Normal file
9
TradingBot/Services/ITradingStrategy.cs
Normal 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);
|
||||
}
|
||||
96
TradingBot/Services/SettingsService.cs
Normal file
96
TradingBot/Services/SettingsService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
68
TradingBot/Services/SimpleMovingAverageStrategy.cs
Normal file
68
TradingBot/Services/SimpleMovingAverageStrategy.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
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)
|
||||
{
|
||||
if (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
|
||||
});
|
||||
}
|
||||
|
||||
var recentPrices = historicalPrices.OrderByDescending(p => p.Timestamp).Take(_longPeriod).ToList();
|
||||
|
||||
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
TradingBot/Services/SimulatedMarketDataService.cs
Normal file
209
TradingBot/Services/SimulatedMarketDataService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
73
TradingBot/Services/TechnicalAnalysis.cs
Normal file
73
TradingBot/Services/TechnicalAnalysis.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
488
TradingBot/Services/TradingBotService.cs
Normal file
488
TradingBot/Services/TradingBotService.cs
Normal file
@@ -0,0 +1,488 @@
|
||||
using TradingBot.Models;
|
||||
|
||||
namespace TradingBot.Services;
|
||||
|
||||
public class TradingBotService
|
||||
{
|
||||
private readonly IMarketDataService _marketDataService;
|
||||
private readonly ITradingStrategy _strategy;
|
||||
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 Timer? _timer;
|
||||
|
||||
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 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)
|
||||
{
|
||||
_marketDataService = marketDataService;
|
||||
_strategy = strategy;
|
||||
Status.CurrentStrategy = strategy.Name;
|
||||
|
||||
// Subscribe to simulated market updates if available
|
||||
if (_marketDataService is SimulatedMarketDataService simService)
|
||||
{
|
||||
simService.OnPriceUpdated += HandleSimulatedPriceUpdate;
|
||||
}
|
||||
|
||||
InitializeDefaultAssets();
|
||||
}
|
||||
|
||||
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, // Enable ALL assets by default for full simulation
|
||||
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;
|
||||
|
||||
// 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));
|
||||
|
||||
OnStatusChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (!Status.IsRunning) return;
|
||||
|
||||
Status.IsRunning = false;
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
|
||||
OnStatusChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleSimulatedPriceUpdate()
|
||||
{
|
||||
if (Status.IsRunning)
|
||||
{
|
||||
_ = UpdateAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var enabledSymbols = _assetConfigs.Values
|
||||
.Where(c => c.IsEnabled)
|
||||
.Select(c => c.Symbol)
|
||||
.ToList();
|
||||
|
||||
if (enabledSymbols.Count == 0) return;
|
||||
|
||||
var prices = await _marketDataService.GetMarketPricesAsync(enabledSymbols);
|
||||
|
||||
foreach (var price in prices)
|
||||
{
|
||||
await ProcessAssetUpdate(price);
|
||||
}
|
||||
|
||||
UpdateGlobalStatistics();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in UpdateAsync: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessAssetUpdate(MarketPrice price)
|
||||
{
|
||||
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]);
|
||||
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)
|
||||
{
|
||||
ExecuteBuy(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)
|
||||
{
|
||||
ExecuteSell(symbol, price.Price, config.CurrentHoldings, config);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ExecuteBuy(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);
|
||||
UpdateAssetStatistics(symbol, trade);
|
||||
|
||||
Status.TradesExecuted++;
|
||||
OnTradeExecuted?.Invoke(trade);
|
||||
OnStatusChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void ExecuteSell(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);
|
||||
UpdateAssetStatistics(symbol, trade, profit);
|
||||
|
||||
Status.TradesExecuted++;
|
||||
OnTradeExecuted?.Invoke(trade);
|
||||
OnStatusChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void UpdateIndicators(string symbol)
|
||||
{
|
||||
var history = _priceHistory[symbol];
|
||||
if (history.Count < 26) return;
|
||||
|
||||
var prices = history.Select(p => p.Price).ToList();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var history = GetPriceHistory(symbol);
|
||||
return history?.LastOrDefault();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user