Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 102 additions & 13 deletions csharp/TraderBot/TradingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class TradingService : BackgroundService
protected long LastWaitOutputTicks;
protected TimeSpan MinimumTimeToBuy;
protected TimeSpan MaximumTimeToBuy;
protected TimeSpan AutoSellMarketCloseTime;
protected readonly ConcurrentDictionary<string, OrderState> ActiveBuyOrders;
protected readonly ConcurrentDictionary<string, OrderState> ActiveSellOrders;
protected readonly ConcurrentDictionary<decimal, long> LotsSets;
Expand All @@ -60,6 +61,11 @@ public TradingService(ILogger<TradingService> 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}");
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions csharp/TraderBot/TradingSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
6 changes: 5 additions & 1 deletion csharp/TraderBot/appsettings.TMON.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
6 changes: 5 additions & 1 deletion csharp/TraderBot/appsettings.TRUR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
45 changes: 45 additions & 0 deletions examples/appsettings.example.json
Original file line number Diff line number Diff line change
@@ -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
}
}
52 changes: 52 additions & 0 deletions examples/test-new-features.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading