diff --git a/csharp/TraderBot/TradingService.cs b/csharp/TraderBot/TradingService.cs index 0302809b..7d352ced 100644 --- a/csharp/TraderBot/TradingService.cs +++ b/csharp/TraderBot/TradingService.cs @@ -63,6 +63,7 @@ public TradingService(ILogger logger, InvestApiClient investApi, Logger.LogInformation($"EarlySellOwnedLotsDelta: {settings.EarlySellOwnedLotsDelta}"); Logger.LogInformation($"EarlySellOwnedLotsMultiplier: {settings.EarlySellOwnedLotsMultiplier}"); Logger.LogInformation($"LoadOperationsFrom: {settings.LoadOperationsFrom}"); + Logger.LogInformation($"MaximumLossPercentage: {settings.MaximumLossPercentage}"); var currentTime = DateTime.UtcNow.TimeOfDay; Logger.LogInformation($"Current time: {currentTime}"); @@ -441,6 +442,44 @@ await marketDataStream.RequestStream.WriteAsync(new MarketDataRequest // Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}."); + // Check for maximum loss protection + if (LotsSets.Count > 0 && ShouldTriggerMaximumLossProtection(bestBid)) + { + Logger.LogCritical($"MAXIMUM LOSS PROTECTION TRIGGERED! Current market price: {bestBid}"); + + // Cancel all existing orders first + var allOrders = new List(); + allOrders.AddRange(ActiveBuyOrders.Keys); + allOrders.AddRange(ActiveSellOrders.Keys); + + foreach (var orderId in allOrders) + { + await TryCancelOrder(orderId); + } + + // Clear active orders + ActiveBuyOrders.Clear(); + ActiveSellOrders.Clear(); + ActiveSellOrderSourcePrice.Clear(); + + // Sell all lots at market price + var totalLots = LotsSets.Values.Sum(); + if (totalLots > 0) + { + await PlaceMarketSellOrder(totalLots); + + // Clear lots as they will be sold + LotsSets.Clear(); + } + + // Reset cash balance + SetCashBalance(CashBalanceFree + CashBalanceLocked, 0); + + Logger.LogCritical($"MAXIMUM LOSS PROTECTION: Sold {totalLots} lots at market price to minimize further losses"); + + continue; + } + // Logger.LogInformation($"Time: {DateTime.Now}"); // Logger.LogInformation($"ActiveBuyOrders.Count: {ActiveBuyOrders.Count}"); // Logger.LogInformation($"ActiveSellOrders.Count: {ActiveSellOrders.Count}"); @@ -691,6 +730,51 @@ private decimal GetTargetSellPrice(decimal minimumSellPrice, decimal bestAsk) return targetSellPrice; } + private decimal CalculateCurrentLossPercentage(decimal currentMarketPrice) + { + if (LotsSets.Count == 0) + { + return 0; + } + + decimal totalCost = 0; + decimal totalLots = 0; + + foreach (var lotsSet in LotsSets) + { + decimal purchasePrice = lotsSet.Key; + long lots = lotsSet.Value; + totalCost += purchasePrice * lots; + totalLots += lots; + } + + if (totalLots == 0) + { + return 0; + } + + decimal averagePurchasePrice = totalCost / totalLots; + decimal currentValue = currentMarketPrice * totalLots; + decimal totalPurchaseCost = averagePurchasePrice * totalLots; + + decimal lossPercentage = ((totalPurchaseCost - currentValue) / totalPurchaseCost) * 100; + + Logger.LogInformation($"Average purchase price: {averagePurchasePrice}, Current price: {currentMarketPrice}, Loss: {lossPercentage:F2}%"); + + return lossPercentage; + } + + private bool ShouldTriggerMaximumLossProtection(decimal currentMarketPrice) + { + if (!Settings.MaximumLossPercentage.HasValue || LotsSets.Count == 0) + { + return false; + } + + decimal currentLoss = CalculateCurrentLossPercentage(currentMarketPrice); + return currentLoss >= Settings.MaximumLossPercentage.Value; + } + protected override async Task ExecuteAsync(CancellationToken cancellationToken) { var tasks = new [] @@ -831,6 +915,22 @@ private async Task PlaceSellOrder(long amount, decimal price) return response; } + private async Task PlaceMarketSellOrder(long amount) + { + PostOrderRequest marketSellOrderRequest = new() + { + OrderId = Guid.NewGuid().ToString(), + AccountId = CurrentAccount.Id, + Direction = OrderDirection.Sell, + OrderType = OrderType.Market, + Figi = Figi, + Quantity = amount + }; + var response = await InvestApi.Orders.PostOrderAsync(marketSellOrderRequest).ResponseAsync; + Logger.LogCritical($"MAXIMUM LOSS PROTECTION: Market sell order placed for {amount} lots: {response}"); + return response; + } + private async Task PlaceBuyOrder(long amount, decimal price) { PostOrderRequest buyOrderRequest = new() diff --git a/csharp/TraderBot/TradingSettings.cs b/csharp/TraderBot/TradingSettings.cs index 884a25df..be5f1698 100644 --- a/csharp/TraderBot/TradingSettings.cs +++ b/csharp/TraderBot/TradingSettings.cs @@ -17,4 +17,5 @@ public class TradingSettings public long EarlySellOwnedLotsDelta { get; set; } public decimal EarlySellOwnedLotsMultiplier { get; set; } public DateTime LoadOperationsFrom { get; set; } + public decimal? MaximumLossPercentage { get; set; } } \ No newline at end of file diff --git a/csharp/TraderBot/appsettings.TMON.json b/csharp/TraderBot/appsettings.TMON.json index c7b66d7a..dacd49b0 100644 --- a/csharp/TraderBot/appsettings.TMON.json +++ b/csharp/TraderBot/appsettings.TMON.json @@ -24,6 +24,7 @@ "MaximumTimeToBuy": "23:59:59", "EarlySellOwnedLotsDelta": 300000, "EarlySellOwnedLotsMultiplier": 0, - "LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z" + "LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z", + "MaximumLossPercentage": 10.0 } } diff --git a/csharp/TraderBot/appsettings.TRUR.json b/csharp/TraderBot/appsettings.TRUR.json index 1dc848e6..cb89b615 100644 --- a/csharp/TraderBot/appsettings.TRUR.json +++ b/csharp/TraderBot/appsettings.TRUR.json @@ -24,6 +24,7 @@ "MaximumTimeToBuy": "14:45:00", "EarlySellOwnedLotsDelta": 300000, "EarlySellOwnedLotsMultiplier": 0, - "LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z" + "LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z", + "MaximumLossPercentage": 10.0 } }