From c0d54f35f5a068456ebb962f6d3a1d9d7706aca4 Mon Sep 17 00:00:00 2001 From: Crypto Od Date: Sun, 2 Mar 2025 20:13:24 +0100 Subject: [PATCH] Update bot management --- .../Workers/PositionManagerWorker.cs | 229 +++++++++++------- src/Managing.Common/Enums.cs | 6 + 2 files changed, 152 insertions(+), 83 deletions(-) diff --git a/src/Managing.Api.Workers/Workers/PositionManagerWorker.cs b/src/Managing.Api.Workers/Workers/PositionManagerWorker.cs index 19f4483..2defca8 100644 --- a/src/Managing.Api.Workers/Workers/PositionManagerWorker.cs +++ b/src/Managing.Api.Workers/Workers/PositionManagerWorker.cs @@ -39,120 +39,183 @@ public class PositionManagerWorker : BaseWorker protected override async Task Run(CancellationToken cancellationToken) { await ManageNewPositions(); - await ManagePartillyFilledPositions(); + await ManagePartiallyFilledPositions(); await ManageFilledPositions(); } - private async Task ManagePartillyFilledPositions() + private async Task ManagePartiallyFilledPositions() { var positions = GetPositions(PositionStatus.PartiallyFilled); - - _logger.LogInformation("Partilly filled positions count : {0} ", positions.Count()); + _logger.LogInformation("Processing {PartiallyFilledCount} partially filled positions", positions.Count()); foreach (var position in positions) { - _logger.LogInformation("Managing Partilly filled position: {0} - Date: {1} - Direction: {2} - Ticker: {3}", - position.Identifier, position.Date, position.OriginDirection, position.Ticker); - - var account = await _accountService.GetAccount(position.AccountName, false, false); - - try + using (_logger.BeginScope("Position {PositionId} ({Ticker})", position.Identifier, position.Ticker)) { - if (position.StopLoss.Status == TradeStatus.PendingOpen) + try { - var stopLoss = await _exchangeService.OpenStopLoss(account, position.Ticker, - position.OriginDirection, - position.StopLoss.Price, position.StopLoss.Quantity, false, DateTime.UtcNow); + // Lock position for processing + position.Status = PositionStatus.Updating; + _tradingService.UpdatePosition(position); + + _logger.LogDebug("Processing risk orders for {Direction} position opened at {OpenDate}", + position.OriginDirection, position.Date.ToString("o")); - if (stopLoss != null & (stopLoss.Status == TradeStatus.Requested)) - { - position.StopLoss = stopLoss; - _logger.LogInformation("|_ SL is requested"); - } - else - { - throw new Exception("Stop loss not requested"); - } - } - else - { - _logger.LogInformation($"|_ SL is already handle. Current status = {position.StopLoss.Status}"); - } + var account = await _accountService.GetAccount(position.AccountName, false, false); + var success = true; - if (position.TakeProfit1.Status == TradeStatus.PendingOpen) - { - var takeProfit1 = await _exchangeService.OpenTakeProfit(account, position.Ticker, - position.OriginDirection, position.TakeProfit1.Price, position.TakeProfit1.Quantity, false, - DateTime.UtcNow); + // Process and update trades + var updatedSl = await ProcessTrade(account, position.StopLoss, "SL", async () => + await _exchangeService.OpenStopLoss(account, position.Ticker, position.OriginDirection, + position.StopLoss.Price, position.StopLoss.Quantity, false, DateTime.UtcNow)); + + if (updatedSl != null) + { + position.StopLoss = updatedSl; + success &= updatedSl.Status.IsActive(); + } - if (takeProfit1 != null & (takeProfit1.Status == TradeStatus.Requested)) + var updatedTp1 = await ProcessTrade(account, position.TakeProfit1, "TP1", async () => + await _exchangeService.OpenTakeProfit(account, position.Ticker, position.OriginDirection, + position.TakeProfit1.Price, position.TakeProfit1.Quantity, false, DateTime.UtcNow)); + + if (updatedTp1 != null) { - position.TakeProfit1 = takeProfit1; - _logger.LogInformation("|_ TP is requested"); + position.TakeProfit1 = updatedTp1; + success &= updatedTp1.Status.IsActive(); } - else - { - throw new Exception("Take Profit 1 not requested"); - } - } - else - { - _logger.LogInformation($"|_ TP is already handle. Current status = {position.TakeProfit1.Status}"); - } - if (position.TakeProfit2 != null && - position.TakeProfit2.Status == TradeStatus.PendingOpen) - { - var takeProfit2 = await _exchangeService.OpenTakeProfit(account, position.Ticker, - position.OriginDirection, position.TakeProfit2.Price, position.TakeProfit2.Quantity, false, - DateTime.UtcNow); + Trade? updatedTp2 = null; + if (position.TakeProfit2 != null) + { + updatedTp2 = await ProcessTrade(account, position.TakeProfit2, "TP2", async () => + await _exchangeService.OpenTakeProfit(account, position.Ticker, position.OriginDirection, + position.TakeProfit2.Price, position.TakeProfit2.Quantity, false, DateTime.UtcNow)); + + if (updatedTp2 != null) + { + position.TakeProfit2 = updatedTp2; + success &= updatedTp2.Status.IsActive() || updatedTp2.Status == TradeStatus.Cancelled; + } + } - if (takeProfit2 != null & (takeProfit2.Status == TradeStatus.Requested)) - { - position.TakeProfit2 = takeProfit2; - _logger.LogInformation("|_ TP2 is requested"); - } - else - { - throw new Exception("Take Profit 2 not requested"); - } + // Update position status based on trade states + position.Status = success && AllTradesActive(position) + ? PositionStatus.Filled + : PositionStatus.PartiallyFilled; + + _logger.LogInformation("Final position status: {Status}", position.Status); } - else + catch (Exception ex) { - _logger.LogInformation("|_ TP2 is already handle or not required"); + _logger.LogError(ex, "Position processing failed"); + position.Status = PositionStatus.PartiallyFilled; + } + finally + { + _tradingService.UpdatePosition(position); } } - catch (Exception ex) - { - _logger.LogError($"|_ Cannot fully filled position because : {ex.Message}"); - } - - TradeStatus[] validStatus = [TradeStatus.Requested, TradeStatus.Filled]; - if (validStatus.Contains(position.StopLoss.Status) - && (validStatus.Contains(position.TakeProfit1.Status))) - { - position.Status = PositionStatus.Filled; - _logger.LogInformation($"|_ Position is now open and SL/TP are correctly requested"); - } - - _tradingService.UpdatePosition(position); } } + private async Task ProcessTrade(Account account, Trade trade, string tradeType, Func> createTrade) + { + try + { + // 1. Check existing status on exchange + var exchangeTrade = await _exchangeService.GetTrade(account, trade.ExchangeOrderId, trade.Ticker); + if (exchangeTrade != null && exchangeTrade.Status.IsActive()) + { + _logger.LogInformation("{TradeType} already exists on exchange - Status: {Status}", + tradeType, exchangeTrade.Status); + return exchangeTrade; + } + + // 2. Only create new order if in pending state + if (trade.Status != TradeStatus.PendingOpen) + { + _logger.LogWarning("{TradeType} creation skipped - Invalid status: {Status}", + tradeType, trade.Status); + return null; + } + + // 3. Create new order + var newTrade = await createTrade(); + if (newTrade?.Status == TradeStatus.Requested) + { + _logger.LogInformation("{TradeType} successfully created - ExchangeID: {ExchangeOrderId}", + tradeType, newTrade.ExchangeOrderId); + return newTrade; + } + + _logger.LogError("{TradeType} creation failed - Null response or invalid status", tradeType); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "{TradeType} processing failed", tradeType); + return null; + } + } + + private bool AllTradesActive(Position position) + { + return position.StopLoss.Status.IsActive() && + position.TakeProfit1.Status.IsActive() && + (position.TakeProfit2?.Status.IsActive() ?? true); + } private async Task ManageFilledPositions() { var positions = GetPositions(PositionStatus.Filled); - - _logger.LogInformation("Filled positions count : {0} ", positions.Count()); + _logger.LogInformation("Monitoring {FilledPositionCount} filled positions", positions.Count()); foreach (var position in positions) { - _logger.LogInformation("Managing filled position: {0} - Date: {1} - Direction: {2} - Ticker: {3}", - position.Identifier, position.Date, position.OriginDirection, position.Ticker); - var account = await GetAccount(position.AccountName); + using (_logger.BeginScope("Position {PositionId} ({Ticker})", position.Identifier, position.Ticker)) + { + try + { + // Acquire processing lock + _logger.LogDebug("Acquiring position lock"); + position.Status = PositionStatus.Updating; + _tradingService.UpdatePosition(position); - var updatedPosition = await _tradingService.ManagePosition(account, position); - _tradingService.UpdatePosition(updatedPosition); + _logger.LogInformation("Managing filled position - Direction: {Direction}, Open Since: {OpenDate}", + position.OriginDirection, position.Date.ToString("yyyy-MM-dd HH:mm:ss")); + + var account = await GetAccount(position.AccountName); + + // Perform position management + var updatedPosition = await _tradingService.ManagePosition(account, position); + + // Log status changes if they occurred + if (updatedPosition.Status != position.Status) + { + _logger.LogInformation("Position status updated: {OldStatus} → {NewStatus}", + position.Status, updatedPosition.Status); + } + + _tradingService.UpdatePosition(updatedPosition); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to manage position - {ErrorMessage}", ex.Message); + + // Reset status for retry + position.Status = PositionStatus.Filled; + _tradingService.UpdatePosition(position); + } + finally + { + // Ensure lock is always released + if (position.Status == PositionStatus.Updating) + { + position.Status = PositionStatus.Filled; + _tradingService.UpdatePosition(position); + } + } + } } } diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index 00a6cb0..214438a 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -190,6 +190,12 @@ public static class Enums Cancelled = 2, Filled = 3, } + + public static bool IsActive(this TradeStatus status) => + status == TradeStatus.Requested || + status == TradeStatus.Cancelled || + status == TradeStatus.Filled; + public enum PositionStatus {