@@ -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 )
0 commit comments