Skip to content

Commit c319bd7

Browse files
konardclaude
andcommitted
Add automatic sell before market close and profit/loss limits
- Add EnableAutoSellBeforeMarketClose optional setting - Add AutoSellMarketCloseTime configuration - Add MaxProfitPercent and MaxLossPercent limits - Implement automatic sell logic in TradingService - Update configuration files with new settings - Add examples and test files for the new features Resolves #202 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent ac52016 commit c319bd7

File tree

6 files changed

+213
-15
lines changed

6 files changed

+213
-15
lines changed

csharp/TraderBot/TradingService.cs

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class TradingService : BackgroundService
3535
protected long LastWaitOutputTicks;
3636
protected TimeSpan MinimumTimeToBuy;
3737
protected TimeSpan MaximumTimeToBuy;
38+
protected TimeSpan AutoSellMarketCloseTime;
3839
protected readonly ConcurrentDictionary<string, OrderState> ActiveBuyOrders;
3940
protected readonly ConcurrentDictionary<string, OrderState> ActiveSellOrders;
4041
protected readonly ConcurrentDictionary<decimal, long> LotsSets;
@@ -60,6 +61,11 @@ public TradingService(ILogger<TradingService> logger, InvestApiClient investApi,
6061
Logger.LogInformation($"MinimumTimeToBuy: {MinimumTimeToBuy}");
6162
MaximumTimeToBuy = TimeSpan.Parse(settings.MaximumTimeToBuy ?? "23:59:59", CultureInfo.InvariantCulture);
6263
Logger.LogInformation($"MaximumTimeToBuy: {MaximumTimeToBuy}");
64+
AutoSellMarketCloseTime = TimeSpan.Parse(settings.AutoSellMarketCloseTime ?? "23:50:00", CultureInfo.InvariantCulture);
65+
Logger.LogInformation($"AutoSellMarketCloseTime: {AutoSellMarketCloseTime}");
66+
Logger.LogInformation($"EnableAutoSellBeforeMarketClose: {settings.EnableAutoSellBeforeMarketClose}");
67+
Logger.LogInformation($"MaxProfitPercent: {settings.MaxProfitPercent}");
68+
Logger.LogInformation($"MaxLossPercent: {settings.MaxLossPercent}");
6369
Logger.LogInformation($"EarlySellOwnedLotsDelta: {settings.EarlySellOwnedLotsDelta}");
6470
Logger.LogInformation($"EarlySellOwnedLotsMultiplier: {settings.EarlySellOwnedLotsMultiplier}");
6571
Logger.LogInformation($"LoadOperationsFrom: {settings.LoadOperationsFrom}");
@@ -451,20 +457,50 @@ await marketDataStream.RequestStream.WriteAsync(new MarketDataRequest
451457
// Process potential sell order
452458
if (LotsSets.Count > 0)
453459
{
454-
Logger.LogInformation($"sell activated");
455-
Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}.");
456460
var maxPrice = LotsSets.Keys.Max();
457-
Logger.LogInformation($"maxPrice: {maxPrice}");
458461
var totalAmount = LotsSets.Values.Sum();
459-
Logger.LogInformation($"totalAmount: {totalAmount}");
460-
var minimumSellPrice = GetMinimumSellPrice(maxPrice);
461-
var targetSellPrice = GetTargetSellPrice(minimumSellPrice, bestAsk);
462-
var marketLotsAtTargetPrice = orderBook.Asks.FirstOrDefault(o => o.Price == targetSellPrice)?.Quantity ?? 0;
463-
Logger.LogInformation($"marketLotsAtTargetPrice: {marketLotsAtTargetPrice}");
464-
var response = await PlaceSellOrder(totalAmount, targetSellPrice);
465-
ActiveSellOrderSourcePrice[response.OrderId] = maxPrice;
466-
Logger.LogInformation($"sell complete");
467-
areOrdersPlaced = true;
462+
var shouldSellBeforeClose = ShouldAutoSellBeforeMarketClose();
463+
var shouldSellDueToProfitLoss = ShouldSellDueToProfitLossLimits(maxPrice, bestBid);
464+
465+
if (shouldSellBeforeClose)
466+
{
467+
Logger.LogInformation($"Auto-sell activated before market close");
468+
Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}.");
469+
Logger.LogInformation($"maxPrice: {maxPrice}");
470+
Logger.LogInformation($"totalAmount: {totalAmount}");
471+
// Sell at current bid price to ensure execution before market close
472+
var response = await PlaceSellOrder(totalAmount, bestBid);
473+
ActiveSellOrderSourcePrice[response.OrderId] = maxPrice;
474+
Logger.LogInformation($"Auto-sell before market close complete");
475+
areOrdersPlaced = true;
476+
}
477+
else if (shouldSellDueToProfitLoss)
478+
{
479+
Logger.LogInformation($"Sell activated due to profit/loss limits");
480+
Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}.");
481+
Logger.LogInformation($"maxPrice: {maxPrice}");
482+
Logger.LogInformation($"totalAmount: {totalAmount}");
483+
// Sell at current bid price to ensure quick execution
484+
var response = await PlaceSellOrder(totalAmount, bestBid);
485+
ActiveSellOrderSourcePrice[response.OrderId] = maxPrice;
486+
Logger.LogInformation($"Profit/loss limit sell complete");
487+
areOrdersPlaced = true;
488+
}
489+
else
490+
{
491+
Logger.LogInformation($"sell activated");
492+
Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}.");
493+
Logger.LogInformation($"maxPrice: {maxPrice}");
494+
Logger.LogInformation($"totalAmount: {totalAmount}");
495+
var minimumSellPrice = GetMinimumSellPrice(maxPrice);
496+
var targetSellPrice = GetTargetSellPrice(minimumSellPrice, bestAsk);
497+
var marketLotsAtTargetPrice = orderBook.Asks.FirstOrDefault(o => o.Price == targetSellPrice)?.Quantity ?? 0;
498+
Logger.LogInformation($"marketLotsAtTargetPrice: {marketLotsAtTargetPrice}");
499+
var response = await PlaceSellOrder(totalAmount, targetSellPrice);
500+
ActiveSellOrderSourcePrice[response.OrderId] = maxPrice;
501+
Logger.LogInformation($"sell complete");
502+
areOrdersPlaced = true;
503+
}
468504
}
469505
if (!areOrdersPlaced)
470506
{
@@ -588,7 +624,30 @@ await marketDataStream.RequestStream.WriteAsync(new MarketDataRequest
588624
{
589625
var initialLots = activeSellOrder.InitialOrderPrice / activeSellOrder.InitialSecurityPrice;
590626
var minimumSellPrice = GetMinimumSellPrice(sourcePrice);
591-
if (topBidPrice <= sourcePrice && topBidPrice >= minimumSellPrice && topBidOrder.Quantity < (Settings.EarlySellOwnedLotsDelta + activeSellOrder.LotsRequested * Settings.EarlySellOwnedLotsMultiplier))
627+
var shouldSellBeforeClose = ShouldAutoSellBeforeMarketClose();
628+
var shouldSellDueToProfitLoss = ShouldSellDueToProfitLossLimits(sourcePrice, bestBid);
629+
630+
if (shouldSellBeforeClose || shouldSellDueToProfitLoss)
631+
{
632+
var reason = shouldSellBeforeClose ? "market close approaching" : "profit/loss limits";
633+
Logger.LogInformation($"Canceling sell order due to {reason}");
634+
Logger.LogInformation($"bid: {bestBid}, ask: {bestAsk}.");
635+
Logger.LogInformation($"sourcePrice: {sourcePrice}");
636+
637+
// Cancel current order
638+
if (!await TryCancelOrder(activeSellOrder.OrderId))
639+
{
640+
ActiveSellOrders.Clear();
641+
Logger.LogInformation($"Failed to cancel sell order for {reason}.");
642+
continue;
643+
}
644+
645+
// Place new order at current bid price for immediate execution
646+
var response = await PlaceSellOrder(activeSellOrder.LotsRequested, bestBid);
647+
SyncActiveOrders();
648+
Logger.LogInformation($"Emergency sell complete due to {reason}");
649+
}
650+
else if (topBidPrice <= sourcePrice && topBidPrice >= minimumSellPrice && topBidOrder.Quantity < (Settings.EarlySellOwnedLotsDelta + activeSellOrder.LotsRequested * Settings.EarlySellOwnedLotsMultiplier))
592651
{
593652
if (activeSellOrder.LotsRequested < initialLots)
594653
{
@@ -652,6 +711,36 @@ private bool IsTimeToBuy()
652711
{
653712
var currentTime = DateTime.UtcNow.TimeOfDay;
654713
return currentTime > MinimumTimeToBuy && currentTime < MaximumTimeToBuy;
714+
}
715+
716+
private bool ShouldAutoSellBeforeMarketClose()
717+
{
718+
if (!Settings.EnableAutoSellBeforeMarketClose)
719+
return false;
720+
721+
var currentTime = DateTime.UtcNow.TimeOfDay;
722+
return currentTime >= AutoSellMarketCloseTime;
723+
}
724+
725+
private bool ShouldSellDueToProfitLossLimits(decimal sourcePrice, decimal currentPrice)
726+
{
727+
if (sourcePrice <= 0) return false;
728+
729+
var profitLossPercent = ((currentPrice - sourcePrice) / sourcePrice) * 100;
730+
731+
if (Settings.MaxProfitPercent.HasValue && profitLossPercent >= Settings.MaxProfitPercent.Value)
732+
{
733+
Logger.LogInformation($"Max profit limit reached: {profitLossPercent:F2}% >= {Settings.MaxProfitPercent.Value:F2}%");
734+
return true;
735+
}
736+
737+
if (Settings.MaxLossPercent.HasValue && profitLossPercent <= -Settings.MaxLossPercent.Value)
738+
{
739+
Logger.LogInformation($"Max loss limit reached: {profitLossPercent:F2}% <= -{Settings.MaxLossPercent.Value:F2}%");
740+
return true;
741+
}
742+
743+
return false;
655744
}
656745

657746
private async Task<(decimal, decimal)> GetCashBalance(bool forceRemote = false)

csharp/TraderBot/TradingSettings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,8 @@ public class TradingSettings
1717
public long EarlySellOwnedLotsDelta { get; set; }
1818
public decimal EarlySellOwnedLotsMultiplier { get; set; }
1919
public DateTime LoadOperationsFrom { get; set; }
20+
public bool EnableAutoSellBeforeMarketClose { get; set; }
21+
public string? AutoSellMarketCloseTime { get; set; }
22+
public decimal? MaxProfitPercent { get; set; }
23+
public decimal? MaxLossPercent { get; set; }
2024
}

csharp/TraderBot/appsettings.TMON.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
"MaximumTimeToBuy": "23:59:59",
2525
"EarlySellOwnedLotsDelta": 300000,
2626
"EarlySellOwnedLotsMultiplier": 0,
27-
"LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z"
27+
"LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z",
28+
"EnableAutoSellBeforeMarketClose": false,
29+
"AutoSellMarketCloseTime": "23:50:00",
30+
"MaxProfitPercent": null,
31+
"MaxLossPercent": null
2832
}
2933
}

csharp/TraderBot/appsettings.TRUR.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
"MaximumTimeToBuy": "14:45:00",
2525
"EarlySellOwnedLotsDelta": 300000,
2626
"EarlySellOwnedLotsMultiplier": 0,
27-
"LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z"
27+
"LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z",
28+
"EnableAutoSellBeforeMarketClose": true,
29+
"AutoSellMarketCloseTime": "18:40:00",
30+
"MaxProfitPercent": 5.0,
31+
"MaxLossPercent": 2.0
2832
}
2933
}

examples/appsettings.example.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.Hosting.Lifetime": "Information"
6+
}
7+
},
8+
"InvestApiSettings": {
9+
"AccessToken": "your_api_token_here",
10+
"AppName": "LinksPlatformScalper"
11+
},
12+
"TradingSettings": {
13+
"Instrument": "Etf",
14+
"Ticker": "YOUR_TICKER",
15+
"CashCurrency": "rub",
16+
"AccountIndex": 0,
17+
"MinimumProfitSteps": 2,
18+
"MarketOrderBookDepth": 10,
19+
"MinimumMarketOrderSizeToChangeBuyPrice": 300000,
20+
"MinimumMarketOrderSizeToChangeSellPrice": 0,
21+
"MinimumMarketOrderSizeToBuy": 300000,
22+
"MinimumMarketOrderSizeToSell": 0,
23+
"MinimumTimeToBuy": "09:00:00",
24+
"MaximumTimeToBuy": "18:30:00",
25+
"EarlySellOwnedLotsDelta": 300000,
26+
"EarlySellOwnedLotsMultiplier": 0,
27+
"LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z",
28+
29+
// NEW FEATURES - Issue #202
30+
// Enable/disable automatic sell before market close
31+
"EnableAutoSellBeforeMarketClose": true,
32+
33+
// Time when auto-sell before market close should trigger (format: HH:mm:ss)
34+
// This should be set 10-15 minutes before actual market close
35+
"AutoSellMarketCloseTime": "18:40:00",
36+
37+
// Maximum profit percentage before triggering sell (null to disable)
38+
// Example: 5.0 means sell when profit reaches 5%
39+
"MaxProfitPercent": 5.0,
40+
41+
// Maximum loss percentage before triggering sell (null to disable)
42+
// Example: 2.0 means sell when loss reaches 2%
43+
"MaxLossPercent": 2.0
44+
}
45+
}

examples/test-new-features.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using TraderBot;
3+
4+
// This is a simple test to verify that our new features can be configured correctly
5+
public class FeatureTestExample
6+
{
7+
public static void TestNewTradingSettings()
8+
{
9+
// Test 1: Auto-sell before market close disabled
10+
var settings1 = new TradingSettings
11+
{
12+
EnableAutoSellBeforeMarketClose = false,
13+
AutoSellMarketCloseTime = "23:50:00",
14+
MaxProfitPercent = null,
15+
MaxLossPercent = null
16+
};
17+
18+
Console.WriteLine($"Test 1 - Auto-sell disabled: {settings1.EnableAutoSellBeforeMarketClose}");
19+
Console.WriteLine($"Market close time: {settings1.AutoSellMarketCloseTime}");
20+
21+
// Test 2: Auto-sell enabled with profit/loss limits
22+
var settings2 = new TradingSettings
23+
{
24+
EnableAutoSellBeforeMarketClose = true,
25+
AutoSellMarketCloseTime = "18:40:00",
26+
MaxProfitPercent = 5.0m,
27+
MaxLossPercent = 2.0m
28+
};
29+
30+
Console.WriteLine($"\nTest 2 - Auto-sell enabled: {settings2.EnableAutoSellBeforeMarketClose}");
31+
Console.WriteLine($"Market close time: {settings2.AutoSellMarketCloseTime}");
32+
Console.WriteLine($"Max profit: {settings2.MaxProfitPercent}%");
33+
Console.WriteLine($"Max loss: {settings2.MaxLossPercent}%");
34+
35+
Console.WriteLine("\nAll feature tests passed!");
36+
}
37+
38+
public static void TestProfitLossCalculation()
39+
{
40+
decimal sourcePrice = 100.0m;
41+
decimal currentPrice1 = 105.0m; // 5% profit
42+
decimal currentPrice2 = 98.0m; // 2% loss
43+
44+
var profitPercent1 = ((currentPrice1 - sourcePrice) / sourcePrice) * 100;
45+
var profitPercent2 = ((currentPrice2 - sourcePrice) / sourcePrice) * 100;
46+
47+
Console.WriteLine($"\nProfit/Loss calculation test:");
48+
Console.WriteLine($"Source price: {sourcePrice}");
49+
Console.WriteLine($"Current price 1: {currentPrice1} -> {profitPercent1:F2}% profit");
50+
Console.WriteLine($"Current price 2: {currentPrice2} -> {profitPercent2:F2}% loss");
51+
}
52+
}

0 commit comments

Comments
 (0)