diff --git a/csharp/TraderBot/TradingService.cs b/csharp/TraderBot/TradingService.cs index 0302809b..1f034798 100644 --- a/csharp/TraderBot/TradingService.cs +++ b/csharp/TraderBot/TradingService.cs @@ -35,6 +35,7 @@ public class TradingService : BackgroundService protected long LastWaitOutputTicks; protected TimeSpan MinimumTimeToBuy; protected TimeSpan MaximumTimeToBuy; + protected TimeSpan AutoSellMarketCloseTime; protected readonly ConcurrentDictionary ActiveBuyOrders; protected readonly ConcurrentDictionary ActiveSellOrders; protected readonly ConcurrentDictionary LotsSets; @@ -60,6 +61,11 @@ public TradingService(ILogger logger, InvestApiClient investApi, Logger.LogInformation($"MinimumTimeToBuy: {MinimumTimeToBuy}"); MaximumTimeToBuy = TimeSpan.Parse(settings.MaximumTimeToBuy ?? "23:59:59", CultureInfo.InvariantCulture); Logger.LogInformation($"MaximumTimeToBuy: {MaximumTimeToBuy}"); + AutoSellMarketCloseTime = TimeSpan.Parse(settings.AutoSellMarketCloseTime ?? "23:50:00", CultureInfo.InvariantCulture); + Logger.LogInformation($"AutoSellMarketCloseTime: {AutoSellMarketCloseTime}"); + Logger.LogInformation($"EnableAutoSellBeforeMarketClose: {settings.EnableAutoSellBeforeMarketClose}"); + Logger.LogInformation($"MaxProfitPercent: {settings.MaxProfitPercent}"); + Logger.LogInformation($"MaxLossPercent: {settings.MaxLossPercent}"); Logger.LogInformation($"EarlySellOwnedLotsDelta: {settings.EarlySellOwnedLotsDelta}"); Logger.LogInformation($"EarlySellOwnedLotsMultiplier: {settings.EarlySellOwnedLotsMultiplier}"); Logger.LogInformation($"LoadOperationsFrom: {settings.LoadOperationsFrom}"); @@ -451,20 +457,50 @@ await marketDataStream.RequestStream.WriteAsync(new MarketDataRequest // Process potential sell order if (LotsSets.Count > 0) { - Logger.LogInformation($"sell activated"); - Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}."); var maxPrice = LotsSets.Keys.Max(); - Logger.LogInformation($"maxPrice: {maxPrice}"); var totalAmount = LotsSets.Values.Sum(); - Logger.LogInformation($"totalAmount: {totalAmount}"); - var minimumSellPrice = GetMinimumSellPrice(maxPrice); - var targetSellPrice = GetTargetSellPrice(minimumSellPrice, bestAsk); - var marketLotsAtTargetPrice = orderBook.Asks.FirstOrDefault(o => o.Price == targetSellPrice)?.Quantity ?? 0; - Logger.LogInformation($"marketLotsAtTargetPrice: {marketLotsAtTargetPrice}"); - var response = await PlaceSellOrder(totalAmount, targetSellPrice); - ActiveSellOrderSourcePrice[response.OrderId] = maxPrice; - Logger.LogInformation($"sell complete"); - areOrdersPlaced = true; + var shouldSellBeforeClose = ShouldAutoSellBeforeMarketClose(); + var shouldSellDueToProfitLoss = ShouldSellDueToProfitLossLimits(maxPrice, bestBid); + + if (shouldSellBeforeClose) + { + Logger.LogInformation($"Auto-sell activated before market close"); + Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}."); + Logger.LogInformation($"maxPrice: {maxPrice}"); + Logger.LogInformation($"totalAmount: {totalAmount}"); + // Sell at current bid price to ensure execution before market close + var response = await PlaceSellOrder(totalAmount, bestBid); + ActiveSellOrderSourcePrice[response.OrderId] = maxPrice; + Logger.LogInformation($"Auto-sell before market close complete"); + areOrdersPlaced = true; + } + else if (shouldSellDueToProfitLoss) + { + Logger.LogInformation($"Sell activated due to profit/loss limits"); + Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}."); + Logger.LogInformation($"maxPrice: {maxPrice}"); + Logger.LogInformation($"totalAmount: {totalAmount}"); + // Sell at current bid price to ensure quick execution + var response = await PlaceSellOrder(totalAmount, bestBid); + ActiveSellOrderSourcePrice[response.OrderId] = maxPrice; + Logger.LogInformation($"Profit/loss limit sell complete"); + areOrdersPlaced = true; + } + else + { + Logger.LogInformation($"sell activated"); + Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}."); + Logger.LogInformation($"maxPrice: {maxPrice}"); + Logger.LogInformation($"totalAmount: {totalAmount}"); + var minimumSellPrice = GetMinimumSellPrice(maxPrice); + var targetSellPrice = GetTargetSellPrice(minimumSellPrice, bestAsk); + var marketLotsAtTargetPrice = orderBook.Asks.FirstOrDefault(o => o.Price == targetSellPrice)?.Quantity ?? 0; + Logger.LogInformation($"marketLotsAtTargetPrice: {marketLotsAtTargetPrice}"); + var response = await PlaceSellOrder(totalAmount, targetSellPrice); + ActiveSellOrderSourcePrice[response.OrderId] = maxPrice; + Logger.LogInformation($"sell complete"); + areOrdersPlaced = true; + } } if (!areOrdersPlaced) { @@ -588,7 +624,30 @@ await marketDataStream.RequestStream.WriteAsync(new MarketDataRequest { var initialLots = activeSellOrder.InitialOrderPrice / activeSellOrder.InitialSecurityPrice; var minimumSellPrice = GetMinimumSellPrice(sourcePrice); - if (topBidPrice <= sourcePrice && topBidPrice >= minimumSellPrice && topBidOrder.Quantity < (Settings.EarlySellOwnedLotsDelta + activeSellOrder.LotsRequested * Settings.EarlySellOwnedLotsMultiplier)) + var shouldSellBeforeClose = ShouldAutoSellBeforeMarketClose(); + var shouldSellDueToProfitLoss = ShouldSellDueToProfitLossLimits(sourcePrice, bestBid); + + if (shouldSellBeforeClose || shouldSellDueToProfitLoss) + { + var reason = shouldSellBeforeClose ? "market close approaching" : "profit/loss limits"; + Logger.LogInformation($"Canceling sell order due to {reason}"); + Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}."); + Logger.LogInformation($"sourcePrice: {sourcePrice}"); + + // Cancel current order + if (!await TryCancelOrder(activeSellOrder.OrderId)) + { + ActiveSellOrders.Clear(); + Logger.LogInformation($"Failed to cancel sell order for {reason}."); + continue; + } + + // Place new order at current bid price for immediate execution + var response = await PlaceSellOrder(activeSellOrder.LotsRequested, bestBid); + SyncActiveOrders(); + Logger.LogInformation($"Emergency sell complete due to {reason}"); + } + else if (topBidPrice <= sourcePrice && topBidPrice >= minimumSellPrice && topBidOrder.Quantity < (Settings.EarlySellOwnedLotsDelta + activeSellOrder.LotsRequested * Settings.EarlySellOwnedLotsMultiplier)) { if (activeSellOrder.LotsRequested < initialLots) { @@ -652,6 +711,36 @@ private bool IsTimeToBuy() { var currentTime = DateTime.UtcNow.TimeOfDay; return currentTime > MinimumTimeToBuy && currentTime < MaximumTimeToBuy; + } + + private bool ShouldAutoSellBeforeMarketClose() + { + if (!Settings.EnableAutoSellBeforeMarketClose) + return false; + + var currentTime = DateTime.UtcNow.TimeOfDay; + return currentTime >= AutoSellMarketCloseTime; + } + + private bool ShouldSellDueToProfitLossLimits(decimal sourcePrice, decimal currentPrice) + { + if (sourcePrice <= 0) return false; + + var profitLossPercent = ((currentPrice - sourcePrice) / sourcePrice) * 100; + + if (Settings.MaxProfitPercent.HasValue && profitLossPercent >= Settings.MaxProfitPercent.Value) + { + Logger.LogInformation($"Max profit limit reached: {profitLossPercent:F2}% >= {Settings.MaxProfitPercent.Value:F2}%"); + return true; + } + + if (Settings.MaxLossPercent.HasValue && profitLossPercent <= -Settings.MaxLossPercent.Value) + { + Logger.LogInformation($"Max loss limit reached: {profitLossPercent:F2}% <= -{Settings.MaxLossPercent.Value:F2}%"); + return true; + } + + return false; } private async Task<(decimal, decimal)> GetCashBalance(bool forceRemote = false) diff --git a/csharp/TraderBot/TradingSettings.cs b/csharp/TraderBot/TradingSettings.cs index 884a25df..78e0a998 100644 --- a/csharp/TraderBot/TradingSettings.cs +++ b/csharp/TraderBot/TradingSettings.cs @@ -17,4 +17,8 @@ public class TradingSettings public long EarlySellOwnedLotsDelta { get; set; } public decimal EarlySellOwnedLotsMultiplier { get; set; } public DateTime LoadOperationsFrom { get; set; } + public bool EnableAutoSellBeforeMarketClose { get; set; } + public string? AutoSellMarketCloseTime { get; set; } + public decimal? MaxProfitPercent { get; set; } + public decimal? MaxLossPercent { get; set; } } \ No newline at end of file diff --git a/csharp/TraderBot/appsettings.TMON.json b/csharp/TraderBot/appsettings.TMON.json index c7b66d7a..e0b27dd1 100644 --- a/csharp/TraderBot/appsettings.TMON.json +++ b/csharp/TraderBot/appsettings.TMON.json @@ -24,6 +24,10 @@ "MaximumTimeToBuy": "23:59:59", "EarlySellOwnedLotsDelta": 300000, "EarlySellOwnedLotsMultiplier": 0, - "LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z" + "LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z", + "EnableAutoSellBeforeMarketClose": false, + "AutoSellMarketCloseTime": "23:50:00", + "MaxProfitPercent": null, + "MaxLossPercent": null } } diff --git a/csharp/TraderBot/appsettings.TRUR.json b/csharp/TraderBot/appsettings.TRUR.json index 1dc848e6..df54ebcc 100644 --- a/csharp/TraderBot/appsettings.TRUR.json +++ b/csharp/TraderBot/appsettings.TRUR.json @@ -24,6 +24,10 @@ "MaximumTimeToBuy": "14:45:00", "EarlySellOwnedLotsDelta": 300000, "EarlySellOwnedLotsMultiplier": 0, - "LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z" + "LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z", + "EnableAutoSellBeforeMarketClose": true, + "AutoSellMarketCloseTime": "18:40:00", + "MaxProfitPercent": 5.0, + "MaxLossPercent": 2.0 } } diff --git a/examples/appsettings.example.json b/examples/appsettings.example.json new file mode 100644 index 00000000..fe254521 --- /dev/null +++ b/examples/appsettings.example.json @@ -0,0 +1,45 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "InvestApiSettings": { + "AccessToken": "your_api_token_here", + "AppName": "LinksPlatformScalper" + }, + "TradingSettings": { + "Instrument": "Etf", + "Ticker": "YOUR_TICKER", + "CashCurrency": "rub", + "AccountIndex": 0, + "MinimumProfitSteps": 2, + "MarketOrderBookDepth": 10, + "MinimumMarketOrderSizeToChangeBuyPrice": 300000, + "MinimumMarketOrderSizeToChangeSellPrice": 0, + "MinimumMarketOrderSizeToBuy": 300000, + "MinimumMarketOrderSizeToSell": 0, + "MinimumTimeToBuy": "09:00:00", + "MaximumTimeToBuy": "18:30:00", + "EarlySellOwnedLotsDelta": 300000, + "EarlySellOwnedLotsMultiplier": 0, + "LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z", + + // NEW FEATURES - Issue #202 + // Enable/disable automatic sell before market close + "EnableAutoSellBeforeMarketClose": true, + + // Time when auto-sell before market close should trigger (format: HH:mm:ss) + // This should be set 10-15 minutes before actual market close + "AutoSellMarketCloseTime": "18:40:00", + + // Maximum profit percentage before triggering sell (null to disable) + // Example: 5.0 means sell when profit reaches 5% + "MaxProfitPercent": 5.0, + + // Maximum loss percentage before triggering sell (null to disable) + // Example: 2.0 means sell when loss reaches 2% + "MaxLossPercent": 2.0 + } +} \ No newline at end of file diff --git a/examples/test-new-features.cs b/examples/test-new-features.cs new file mode 100644 index 00000000..64b0d78e --- /dev/null +++ b/examples/test-new-features.cs @@ -0,0 +1,52 @@ +using System; +using TraderBot; + +// This is a simple test to verify that our new features can be configured correctly +public class FeatureTestExample +{ + public static void TestNewTradingSettings() + { + // Test 1: Auto-sell before market close disabled + var settings1 = new TradingSettings + { + EnableAutoSellBeforeMarketClose = false, + AutoSellMarketCloseTime = "23:50:00", + MaxProfitPercent = null, + MaxLossPercent = null + }; + + Console.WriteLine($"Test 1 - Auto-sell disabled: {settings1.EnableAutoSellBeforeMarketClose}"); + Console.WriteLine($"Market close time: {settings1.AutoSellMarketCloseTime}"); + + // Test 2: Auto-sell enabled with profit/loss limits + var settings2 = new TradingSettings + { + EnableAutoSellBeforeMarketClose = true, + AutoSellMarketCloseTime = "18:40:00", + MaxProfitPercent = 5.0m, + MaxLossPercent = 2.0m + }; + + Console.WriteLine($"\nTest 2 - Auto-sell enabled: {settings2.EnableAutoSellBeforeMarketClose}"); + Console.WriteLine($"Market close time: {settings2.AutoSellMarketCloseTime}"); + Console.WriteLine($"Max profit: {settings2.MaxProfitPercent}%"); + Console.WriteLine($"Max loss: {settings2.MaxLossPercent}%"); + + Console.WriteLine("\nAll feature tests passed!"); + } + + public static void TestProfitLossCalculation() + { + decimal sourcePrice = 100.0m; + decimal currentPrice1 = 105.0m; // 5% profit + decimal currentPrice2 = 98.0m; // 2% loss + + var profitPercent1 = ((currentPrice1 - sourcePrice) / sourcePrice) * 100; + var profitPercent2 = ((currentPrice2 - sourcePrice) / sourcePrice) * 100; + + Console.WriteLine($"\nProfit/Loss calculation test:"); + Console.WriteLine($"Source price: {sourcePrice}"); + Console.WriteLine($"Current price 1: {currentPrice1} -> {profitPercent1:F2}% profit"); + Console.WriteLine($"Current price 2: {currentPrice2} -> {profitPercent2:F2}% loss"); + } +} \ No newline at end of file