Fix config update + remove messages + Summary fix for not open position

This commit is contained in:
2025-10-08 02:52:11 +07:00
parent ff7e4ed3d3
commit 67065469a6
17 changed files with 209 additions and 159 deletions

View File

@@ -10,6 +10,7 @@ using Managing.Core;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Backtests; using Managing.Domain.Backtests;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Indicators;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Strategies; using Managing.Domain.Strategies;
@@ -479,13 +480,13 @@ public class BotController : BaseController
} }
/// <summary> /// <summary>
/// Manually opens a position for a specified bot with the given parameters. /// Manually create a signal for a specified bot with the given parameters.
/// </summary> /// </summary>
/// <param name="request">The request containing position parameters.</param> /// <param name="request">The request containing position parameters.</param>
/// <returns>A response indicating the result of the operation.</returns> /// <returns>A response indicating the result of the operation.</returns>
[HttpPost] [HttpPost]
[Route("OpenPosition")] [Route("CreateManualSignal")]
public async Task<ActionResult<Position>> OpenPositionManually([FromBody] OpenPositionManuallyRequest request) public async Task<ActionResult<LightSignal>> CreateManualSignalAsync([FromBody] CreateManualSignalRequest request)
{ {
try try
{ {
@@ -507,16 +508,15 @@ public class BotController : BaseController
return BadRequest($"Bot with identifier {request.Identifier} is not running"); return BadRequest($"Bot with identifier {request.Identifier} is not running");
} }
var position = await _botService.OpenPositionManuallyAsync(request.Identifier, request.Direction); var signal = await _botService.CreateManualSignalAsync(request.Identifier, request.Direction);
await NotifyBotSubscriberAsync(); return Ok(signal);
return Ok(position);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error opening position manually"); _logger.LogError(ex, "Error creating signal manually");
return StatusCode(500, return StatusCode(500,
$"Error opening position: {ex.Message}, {ex.InnerException?.Message} or {ex.StackTrace}"); $"Error creating signal: {ex.Message}, {ex.InnerException?.Message} or {ex.StackTrace}");
} }
} }
@@ -664,17 +664,17 @@ public class BotController : BaseController
Leverage = fullMoneyManagement.Leverage Leverage = fullMoneyManagement.Leverage
}; };
} }
else if (request.MoneyManagement != null) else if (request.Config.MoneyManagement != null)
{ {
// Use provided money management object // Use provided money management object
moneyManagement = request.MoneyManagement; moneyManagement = new LightMoneyManagement
// Format percentage values if using custom money management
moneyManagement.FormatPercentage();
}
else
{ {
// Use existing bot's money management if no new one is provided Name = request.Config.Name,
moneyManagement = config.MoneyManagement; Timeframe = request.Config.Timeframe,
StopLoss = request.Config.MoneyManagement.StopLoss,
TakeProfit = request.Config.MoneyManagement.TakeProfit,
Leverage = request.Config.MoneyManagement.Leverage
};
} }
// Validate CloseEarlyWhenProfitable requires MaxPositionTimeHours // Validate CloseEarlyWhenProfitable requires MaxPositionTimeHours
@@ -921,7 +921,7 @@ public class BotController : BaseController
/// <summary> /// <summary>
/// Request model for opening a position manually /// Request model for opening a position manually
/// </summary> /// </summary>
public class OpenPositionManuallyRequest public class CreateManualSignalRequest
{ {
/// <summary> /// <summary>
/// The identifier of the bot /// The identifier of the bot

View File

@@ -1,5 +1,6 @@
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Indicators;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Domain.Users; using Managing.Domain.Users;
using Orleans; using Orleans;
@@ -18,7 +19,7 @@ public interface ILiveTradingBotGrain : IGrainWithGuidKey
/// </summary> /// </summary>
/// <param name="direction">The direction of the trade (Long/Short)</param> /// <param name="direction">The direction of the trade (Long/Short)</param>
/// <returns>The created Position object</returns> /// <returns>The created Position object</returns>
Task<Position> OpenPositionManuallyAsync(TradeDirection direction); Task<LightSignal> CreateManualSignalAsync(TradeDirection direction);
/// <summary> /// <summary>
/// Gets comprehensive bot data including positions, signals, and performance metrics /// Gets comprehensive bot data including positions, signals, and performance metrics

View File

@@ -1,6 +1,7 @@
using Managing.Application.Bots.Models; using Managing.Application.Bots.Models;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Indicators;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -19,7 +20,7 @@ public interface IBotService
Task<IEnumerable<Bot>> GetBotsByIdsAsync(IEnumerable<Guid> botIds); Task<IEnumerable<Bot>> GetBotsByIdsAsync(IEnumerable<Guid> botIds);
Task<Bot> GetBotByName(string name); Task<Bot> GetBotByName(string name);
Task<Bot> GetBotByIdentifier(Guid identifier); Task<Bot> GetBotByIdentifier(Guid identifier);
Task<Position> OpenPositionManuallyAsync(Guid identifier, TradeDirection direction); Task<LightSignal> CreateManualSignalAsync(Guid identifier, TradeDirection direction);
Task<Position> ClosePositionAsync(Guid identifier, Guid positionId); Task<Position> ClosePositionAsync(Guid identifier, Guid positionId);
Task<TradingBotConfig> GetBotConfig(Guid identifier); Task<TradingBotConfig> GetBotConfig(Guid identifier);
Task<IEnumerable<TradingBotConfig>> GetBotConfigsByIdsAsync(IEnumerable<Guid> botIds); Task<IEnumerable<TradingBotConfig>> GetBotConfigsByIdsAsync(IEnumerable<Guid> botIds);

View File

@@ -25,7 +25,7 @@ namespace Managing.Application.Abstractions
decimal GetTotalFees(); decimal GetTotalFees();
Task LoadAccount(); Task LoadAccount();
Task LoadLastCandle(); Task LoadLastCandle();
Task<Position> OpenPositionManually(TradeDirection direction); Task<LightSignal> CreateManualSignal(TradeDirection direction);
Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice, Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice,
bool tradeClosingPosition = false); bool tradeClosingPosition = false);

View File

@@ -6,6 +6,7 @@ using Managing.Application.Shared;
using Managing.Core; using Managing.Core;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Indicators;
using Managing.Domain.Shared.Helpers; using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Domain.Users; using Managing.Domain.Users;
@@ -277,7 +278,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
var hasOpenPositions = await HasOpenPositionsInDatabaseAsync(); var hasOpenPositions = await HasOpenPositionsInDatabaseAsync();
if (hasOpenPositions) if (hasOpenPositions)
{ {
_logger.LogWarning("Stopping bot {Name} while it still has open positions in database. Trading loop will stop but positions remain managed by system.", _logger.LogWarning(
"Stopping bot {Name} while it still has open positions in database. Trading loop will stop but positions remain managed by system.",
_tradingBot?.Config.Name); _tradingBot?.Config.Name);
throw new InvalidOperationException( throw new InvalidOperationException(
"Cannot stop bot while it has open positions. Please close all positions first."); "Cannot stop bot while it has open positions. Please close all positions first.");
@@ -442,7 +444,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
} }
public async Task<Position> OpenPositionManuallyAsync(TradeDirection direction) public async Task<LightSignal> CreateManualSignalAsync(TradeDirection direction)
{ {
try try
{ {
@@ -462,7 +464,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
await _state.WriteStateAsync(); await _state.WriteStateAsync();
} }
return await _tradingBot.OpenPositionManually(direction); return await _tradingBot.CreateManualSignal(direction);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -339,11 +339,6 @@ public class TradingBotBase : ITradingBot
{ {
Positions[newlyCreatedPosition.Identifier] = newlyCreatedPosition; Positions[newlyCreatedPosition.Identifier] = newlyCreatedPosition;
} }
else
{
await LogWarning(
$"⚠️ Position Creation Failed\nSignal: `{signal.Identifier}`\nPosition creation returned null");
}
} }
} }
@@ -486,7 +481,7 @@ public class TradingBotBase : ITradingBot
if (timeSinceRequest.TotalMinutes >= waitTimeMinutes) if (timeSinceRequest.TotalMinutes >= waitTimeMinutes)
{ {
await LogWarning( await LogWarning(
$"⚠️ Order Cleanup\nToo many open orders: `{orders.Count()}`\nPosition: `{positionForSignal.Identifier}`\nTime elapsed: `{waitTimeMinutes}min`\nCanceling all orders..."); $"⚠️ Orders Cleanup\nTime elapsed: {waitTimeMinutes}min\nCanceling all orders...");
try try
{ {
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory,
@@ -495,7 +490,7 @@ public class TradingBotBase : ITradingBot
await exchangeService.CancelOrder(Account, Config.Ticker); await exchangeService.CancelOrder(Account, Config.Ticker);
}); });
await LogInformation( await LogInformation(
$"✅ Orders Canceled\nSuccessfully canceled all orders for: `{Config.Ticker}`"); $"✅ Orders for {internalPosition.OriginDirection} {Config.Ticker} successfully canceled");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -504,6 +499,13 @@ public class TradingBotBase : ITradingBot
await SetPositionStatus(signal.Identifier, PositionStatus.Canceled); await SetPositionStatus(signal.Identifier, PositionStatus.Canceled);
SetSignalStatus(signal.Identifier, SignalStatus.Expired); SetSignalStatus(signal.Identifier, SignalStatus.Expired);
positionForSignal.Status = PositionStatus.Canceled;
positionForSignal.Open.SetStatus(TradeStatus.Cancelled);
positionForSignal.StopLoss.SetStatus(TradeStatus.Cancelled);
positionForSignal.TakeProfit1.SetStatus(TradeStatus.Cancelled);
await UpdatePositionDatabase(positionForSignal);
return; return;
} }
else else
@@ -762,16 +764,16 @@ public class TradingBotBase : ITradingBot
} }
} }
} }
else if (internalPosition.Status == PositionStatus.Rejected || // else if (internalPosition.Status == PositionStatus.Rejected ||
internalPosition.Status == PositionStatus.Canceled) // internalPosition.Status == PositionStatus.Canceled)
{ // {
await LogWarning($"Open position trade is rejected for signal {signal.Identifier}"); // await LogWarning($"Open position trade is rejected for signal {signal.Identifier}");
if (signal.Status == SignalStatus.PositionOpen) // if (signal.Status == SignalStatus.PositionOpen)
{ // {
Logger.LogInformation($"Try to re-open position"); // Logger.LogInformation($"Try to re-open position");
await OpenPosition(signal); // await OpenPosition(signal);
} // }
} // }
if (Config.UseSynthApi && !Config.IsForBacktest && if (Config.UseSynthApi && !Config.IsForBacktest &&
positionForSignal.Status == PositionStatus.Filled) positionForSignal.Status == PositionStatus.Filled)
@@ -926,24 +928,26 @@ public class TradingBotBase : ITradingBot
if (position != null) if (position != null)
{ {
if (position.Open.Status != TradeStatus.Cancelled) if (position.Open.Status != TradeStatus.Cancelled && position.Status != PositionStatus.Rejected)
{ {
SetSignalStatus(signal.Identifier, SignalStatus.PositionOpen); SetSignalStatus(signal.Identifier, SignalStatus.PositionOpen);
if (!Config.IsForBacktest) if (!Config.IsForBacktest)
{ {
await ServiceScopeHelpers.WithScopedService<IMessengerService>(_scopeFactory, await ServiceScopeHelpers.WithScopedService<IMessengerService>(_scopeFactory,
async messengerService => { await messengerService.SendClosedPosition(position, Account.User); }); async messengerService => { await messengerService.SendPosition(position); });
} }
Logger.LogInformation($"Position requested"); Logger.LogInformation($"Position requested");
return position; // Return the created position without adding to list return position;
} }
else else
{ {
await SetPositionStatus(signal.Identifier, PositionStatus.Rejected); await SetPositionStatus(signal.Identifier, PositionStatus.Rejected);
position.Status = PositionStatus.Rejected;
await UpdatePositionDatabase(position);
SetSignalStatus(signal.Identifier, SignalStatus.Expired); SetSignalStatus(signal.Identifier, SignalStatus.Expired);
return null; return position;
} }
} }
@@ -964,7 +968,7 @@ public class TradingBotBase : ITradingBot
catch (Exception ex) catch (Exception ex)
{ {
SetSignalStatus(signal.Identifier, SignalStatus.Expired); SetSignalStatus(signal.Identifier, SignalStatus.Expired);
await LogWarning($"Cannot open trade : {ex.Message}, stackTrace : {ex.StackTrace}"); SentrySdk.CaptureException(ex);
return null; return null;
} }
} }
@@ -1586,10 +1590,7 @@ public class TradingBotBase : ITradingBot
if (!Config.IsForBacktest) if (!Config.IsForBacktest)
{ {
await ServiceScopeHelpers.WithScopedService<IMessengerService>(_scopeFactory, await ServiceScopeHelpers.WithScopedService<IMessengerService>(_scopeFactory,
async messengerService => async messengerService => { await messengerService.SendClosedPosition(position, Account.User); });
{
await messengerService.SendClosedPosition(position, Account.User);
});
} }
await CancelAllOrders(); await CancelAllOrders();
@@ -1810,7 +1811,7 @@ public class TradingBotBase : ITradingBot
/// <param name="direction">The direction of the trade (Long/Short).</param> /// <param name="direction">The direction of the trade (Long/Short).</param>
/// <returns>The created Position object.</returns> /// <returns>The created Position object.</returns>
/// <exception cref="Exception">Throws if no candles are available or position opening fails.</exception> /// <exception cref="Exception">Throws if no candles are available or position opening fails.</exception>
public async Task<Position> OpenPositionManually(TradeDirection direction) public async Task<LightSignal> CreateManualSignal(TradeDirection direction)
{ {
if (LastCandle == null) if (LastCandle == null)
{ {
@@ -1828,22 +1829,7 @@ public class TradingBotBase : ITradingBot
// Add the signal to our collection // Add the signal to our collection
await AddSignal(signal); await AddSignal(signal);
// Open the position using the generated signal (SL/TP handled by MoneyManagement) return signal;
var position = await OpenPosition(signal);
if (position == null)
{
// Clean up the signal if position creation failed
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
throw new Exception("Failed to open position");
}
// Add the position to the list after successful creation
Positions[position.Identifier] = position;
Logger.LogInformation(
$"👤 Manual Position Opened\nPosition: `{position.Identifier}`\nSignal: `{signal.Identifier}`\nAdded to positions list");
return position;
} }
public async Task AddSignal(LightSignal signal) public async Task AddSignal(LightSignal signal)
@@ -1866,7 +1852,7 @@ public class TradingBotBase : ITradingBot
$"🆔 Signal ID: `{signal.Identifier}`"; $"🆔 Signal ID: `{signal.Identifier}`";
// Apply Synth-based signal filtering if enabled // Apply Synth-based signal filtering if enabled
if ((Config.UseSynthApi || !Config.IsForBacktest) && ExecutionCount > 0) if (Config.UseSynthApi && !Config.IsForBacktest && ExecutionCount > 0)
{ {
await ServiceScopeHelpers.WithScopedServices<ITradingService, IExchangeService>(_scopeFactory, await ServiceScopeHelpers.WithScopedServices<ITradingService, IExchangeService>(_scopeFactory,
async (tradingService, exchangeService) => async (tradingService, exchangeService) =>
@@ -2052,11 +2038,26 @@ public class TradingBotBase : ITradingBot
changes.Add($"👀 Watch Only: {oldWatch} → {newWatch}"); changes.Add($"👀 Watch Only: {oldWatch} → {newWatch}");
} }
if (Config.MoneyManagement?.GetType().Name != newConfig.MoneyManagement?.GetType().Name) // Check for changes in individual MoneyManagement properties
if (Config.MoneyManagement?.StopLoss != newConfig.MoneyManagement?.StopLoss)
{ {
var oldMM = Config.MoneyManagement?.GetType().Name ?? "None"; var oldStopLoss = Config.MoneyManagement?.StopLoss.ToString("P2") ?? "None";
var newMM = newConfig.MoneyManagement?.GetType().Name ?? "None"; var newStopLoss = newConfig.MoneyManagement?.StopLoss.ToString("P2") ?? "None";
changes.Add($"💰 Money Management: {oldMM} → {newMM}"); changes.Add($"🛑 Stop Loss: {oldStopLoss} → {newStopLoss}");
}
if (Config.MoneyManagement?.TakeProfit != newConfig.MoneyManagement?.TakeProfit)
{
var oldTakeProfit = Config.MoneyManagement?.TakeProfit.ToString("P2") ?? "None";
var newTakeProfit = newConfig.MoneyManagement?.TakeProfit.ToString("P2") ?? "None";
changes.Add($"🎯 Take Profit: {oldTakeProfit} → {newTakeProfit}");
}
if (Config.MoneyManagement?.Leverage != newConfig.MoneyManagement?.Leverage)
{
var oldLeverage = Config.MoneyManagement?.Leverage.ToString("F1") + "x" ?? "None";
var newLeverage = newConfig.MoneyManagement?.Leverage.ToString("F1") + "x" ?? "None";
changes.Add($"⚡ Leverage: {oldLeverage} → {newLeverage}");
} }
if (Config.RiskManagement != newConfig.RiskManagement) if (Config.RiskManagement != newConfig.RiskManagement)

View File

@@ -136,12 +136,14 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
foreach (var position in positions) foreach (var position in positions)
{ {
if (!position.IsValidForMetrics()) continue;
// Calculate volume using the dedicated method // Calculate volume using the dedicated method
var positionVolume = TradingHelpers.GetVolumeForPosition(position); var positionVolume = TradingHelpers.GetVolumeForPosition(position);
totalVolume += positionVolume; totalVolume += positionVolume;
// Add to open interest for active positions only (only opening volume) // Add to open interest for active positions only (only opening volume)
if (!position.IsFinished()) if (position.Status.Equals(PositionStatus.Filled))
{ {
var openingVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage; var openingVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage;
totalOpenInterest += openingVolume; totalOpenInterest += openingVolume;
@@ -175,7 +177,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
_state.State.PositionCountByAsset[ticker]++; _state.State.PositionCountByAsset[ticker]++;
// Position count breakdown by direction - only count finished positions // Position count breakdown by direction - only count finished positions
if (!position.IsFinished()) if (position.IsValidForMetrics())
{ {
if (!_state.State.PositionCountByDirection.ContainsKey(direction)) if (!_state.State.PositionCountByDirection.ContainsKey(direction))
{ {
@@ -201,6 +203,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
_state.State.PositionCountByDirection.GetValueOrDefault(TradeDirection.Short, 0)); _state.State.PositionCountByDirection.GetValueOrDefault(TradeDirection.Short, 0));
_state.State.LastUpdated = DateTime.UtcNow; _state.State.LastUpdated = DateTime.UtcNow;
await RefreshAgentCountAsync();
await _state.WriteStateAsync(); await _state.WriteStateAsync();
_logger.LogInformation("Platform summary data refreshed successfully"); _logger.LogInformation("Platform summary data refreshed successfully");
@@ -420,7 +423,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
var snapshot = await CalculateDailySnapshotFromPositionsAsync(positions.ToList(), missingDate); var snapshot = await CalculateDailySnapshotFromPositionsAsync(positions.ToList(), missingDate);
_state.State.DailySnapshots.Add(snapshot); _state.State.DailySnapshots.Add(snapshot);
_logger.LogInformation("Created missing daily snapshot for {Date}: Volume={Volume}, PnL={PnL}, Positions={Positions}", _logger.LogInformation(
"Created missing daily snapshot for {Date}: Volume={Volume}, PnL={PnL}, Positions={Positions}",
missingDate, snapshot.TotalVolume, snapshot.TotalPnL, snapshot.TotalLifetimePositionCount); missingDate, snapshot.TotalVolume, snapshot.TotalPnL, snapshot.TotalLifetimePositionCount);
} }
@@ -448,7 +452,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
/// <param name="positions">All positions to analyze</param> /// <param name="positions">All positions to analyze</param>
/// <param name="targetDate">The date to calculate the snapshot up to</param> /// <param name="targetDate">The date to calculate the snapshot up to</param>
/// <returns>A cumulative daily snapshot for the specified date</returns> /// <returns>A cumulative daily snapshot for the specified date</returns>
private async Task<DailySnapshot> CalculateDailySnapshotFromPositionsAsync(List<Position> positions, DateTime targetDate) private async Task<DailySnapshot> CalculateDailySnapshotFromPositionsAsync(List<Position> positions,
DateTime targetDate)
{ {
var dayStart = targetDate; var dayStart = targetDate;
var dayEnd = targetDate.AddDays(1); var dayEnd = targetDate.AddDays(1);
@@ -480,9 +485,13 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
// Check if position was active at this hour // Check if position was active at this hour
var wasActiveAtThisHour = position.Date <= hourDateTime && var wasActiveAtThisHour = position.Date <= hourDateTime &&
(!position.IsFinished() || (!position.IsFinished() ||
(position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date > hourDateTime) || (position.StopLoss.Status == TradeStatus.Filled &&
(position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date > hourDateTime) || position.StopLoss.Date > hourDateTime) ||
(position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date > hourDateTime)); (position.TakeProfit1.Status == TradeStatus.Filled &&
position.TakeProfit1.Date > hourDateTime) ||
(position.TakeProfit2 != null &&
position.TakeProfit2.Status == TradeStatus.Filled &&
position.TakeProfit2.Date > hourDateTime));
if (wasActiveAtThisHour) if (wasActiveAtThisHour)
{ {
@@ -502,7 +511,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
{ {
// Calculate CUMULATIVE volume up to this point in time // Calculate CUMULATIVE volume up to this point in time
// Include all positions that were opened on or before the target date // Include all positions that were opened on or before the target date
_logger.LogDebug("Checking position {PositionId}: Position.Date={PositionDate}, TargetDate={TargetDate}, Position.Date.Date={PositionDateOnly}", _logger.LogDebug(
"Checking position {PositionId}: Position.Date={PositionDate}, TargetDate={TargetDate}, Position.Date.Date={PositionDateOnly}",
position.Identifier, position.Date, targetDate, position.Date.Date); position.Identifier, position.Date, targetDate, position.Date.Date);
// Add opening volume if position was opened on or before this day // Add opening volume if position was opened on or before this day
@@ -511,7 +521,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
{ {
var openingVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage; var openingVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage;
totalVolume += openingVolume; totalVolume += openingVolume;
_logger.LogDebug("Position {PositionId} opened on/before {TargetDate}: Opening volume = {OpeningVolume}", _logger.LogDebug(
"Position {PositionId} opened on/before {TargetDate}: Opening volume = {OpeningVolume}",
position.Identifier, targetDate, openingVolume); position.Identifier, targetDate, openingVolume);
} }
@@ -520,21 +531,29 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
{ {
if (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate) if (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate)
{ {
var closingVolume = position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage; var closingVolume = position.StopLoss.Price * position.StopLoss.Quantity *
position.StopLoss.Leverage;
totalVolume += closingVolume; totalVolume += closingVolume;
_logger.LogDebug("Position {PositionId} closed on/before {TargetDate} via StopLoss: Closing volume = {ClosingVolume}", _logger.LogDebug(
"Position {PositionId} closed on/before {TargetDate} via StopLoss: Closing volume = {ClosingVolume}",
position.Identifier, targetDate, closingVolume); position.Identifier, targetDate, closingVolume);
} }
if (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate) if (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate)
{ {
var closingVolume = position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage; var closingVolume = position.TakeProfit1.Price * position.TakeProfit1.Quantity *
position.TakeProfit1.Leverage;
totalVolume += closingVolume; totalVolume += closingVolume;
_logger.LogDebug("Position {PositionId} closed on/before {TargetDate} via TakeProfit1: Closing volume = {ClosingVolume}", _logger.LogDebug(
"Position {PositionId} closed on/before {TargetDate} via TakeProfit1: Closing volume = {ClosingVolume}",
position.Identifier, targetDate, closingVolume); position.Identifier, targetDate, closingVolume);
} }
if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date <= targetDate)
if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled &&
position.TakeProfit2.Date.Date <= targetDate)
{ {
var closingVolume = position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage; var closingVolume = position.TakeProfit2.Price * position.TakeProfit2.Quantity *
position.TakeProfit2.Leverage;
totalVolume += closingVolume; totalVolume += closingVolume;
} }
} }
@@ -543,7 +562,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
var wasClosedOnOrBeforeThisDay = position.IsFinished() && ( var wasClosedOnOrBeforeThisDay = position.IsFinished() && (
(position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate) || (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate) ||
(position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate) || (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate) ||
(position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date <= targetDate) (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled &&
position.TakeProfit2.Date.Date <= targetDate)
); );
if (wasClosedOnOrBeforeThisDay) if (wasClosedOnOrBeforeThisDay)
@@ -563,7 +583,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
var totalAgents = await _agentService.GetTotalAgentCount(); var totalAgents = await _agentService.GetTotalAgentCount();
var totalStrategies = _state.State.TotalActiveStrategies; var totalStrategies = _state.State.TotalActiveStrategies;
_logger.LogInformation("Calculated CUMULATIVE daily snapshot for {TargetDate}: CumVolume={TotalVolume}, MaxOpenInterest={MaxOpenInterest}, CumPositionCount={TotalPositionCount}", _logger.LogInformation(
"Calculated CUMULATIVE daily snapshot for {TargetDate}: CumVolume={TotalVolume}, MaxOpenInterest={MaxOpenInterest}, CumPositionCount={TotalPositionCount}",
targetDate, totalVolume, maxOpenInterest, totalPositionCount); targetDate, totalVolume, maxOpenInterest, totalPositionCount);
return new DailySnapshot return new DailySnapshot

View File

@@ -8,6 +8,7 @@ using Managing.Common;
using Managing.Core; using Managing.Core;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Indicators;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers; using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades; using Managing.Domain.Trades;
@@ -264,10 +265,10 @@ namespace Managing.Application.ManageBot
return await _botRepository.GetBotByIdentifierAsync(identifier); return await _botRepository.GetBotByIdentifierAsync(identifier);
} }
public async Task<Position> OpenPositionManuallyAsync(Guid identifier, TradeDirection direction) public async Task<LightSignal> CreateManualSignalAsync(Guid identifier, TradeDirection direction)
{ {
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier); var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
return await grain.OpenPositionManuallyAsync(direction); return await grain.CreateManualSignalAsync(direction);
} }
public async Task<Position> ClosePositionAsync(Guid identifier, Guid positionId) public async Task<Position> ClosePositionAsync(Guid identifier, Guid positionId)

View File

@@ -73,7 +73,9 @@ namespace Managing.Domain.Trades
/// <summary> /// <summary>
/// Identifier of the bot or entity that initiated this position /// Identifier of the bot or entity that initiated this position
/// </summary> /// </summary>
[Id(17)] [Required] public Guid InitiatorIdentifier { get; set; } [Id(17)]
[Required]
public Guid InitiatorIdentifier { get; set; }
public bool IsFinished() public bool IsFinished()
{ {
@@ -85,6 +87,18 @@ namespace Managing.Domain.Trades
}; };
} }
public bool IsValidForMetrics()
{
return Status switch
{
PositionStatus.Filled => true,
PositionStatus.Finished => true,
PositionStatus.Flipped => true,
PositionStatus.Updating => true,
_ => false
};
}
/// <summary> /// <summary>
/// Calculates the total fees for this position based on GMX V2 fee structure /// Calculates the total fees for this position based on GMX V2 fee structure
/// </summary> /// </summary>

View File

@@ -671,7 +671,7 @@ public class EvmManager : IEvmManager
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); SentrySdk.CaptureException(e);
throw; throw;
} }
} }

View File

@@ -9,8 +9,8 @@ test('GMX Position Closing', async (t) => {
const result = await closeGmxPositionImpl( const result = await closeGmxPositionImpl(
sdk, sdk,
"DOGE", "ADA",
TradeDirection.Short TradeDirection.Long
) )
console.log('Position closing result:', result) console.log('Position closing result:', result)
assert.ok(result, 'Position closing result should be defined') assert.ok(result, 'Position closing result should be defined')

View File

@@ -30,17 +30,17 @@ function ManualPositionModal({ showModal, botName, onClose }: ManualPositionModa
const onSubmit = async (data: ManualPositionFormValues) => { const onSubmit = async (data: ManualPositionFormValues) => {
if (!botName) return if (!botName) return
const t = new Toast('Opening position...') const t = new Toast('Creating signal...')
try { try {
await client.bot_OpenPositionManually({ await client.bot_CreateManualSignal({
identifier: botName, identifier: botName,
direction: data.direction, direction: data.direction,
}) })
t.update('success', 'Position opened successfully') t.update('success', 'Signal created successfully')
reset() reset()
onClose() onClose()
} catch (error: any) { } catch (error: any) {
t.update('error', `Failed to open position: ${error.message || error}`) t.update('error', `Failed to create signal: ${error.message || error}`)
} }
} }
@@ -49,7 +49,7 @@ function ManualPositionModal({ showModal, botName, onClose }: ManualPositionModa
return ( return (
<dialog open={showModal} className="modal modal-bottom sm:modal-middle modal-open"> <dialog open={showModal} className="modal modal-bottom sm:modal-middle modal-open">
<div className="modal-box"> <div className="modal-box">
<h3 className="font-bold text-lg">Open Position Manually for {botName}</h3> <h3 className="font-bold text-lg">Create Signal Manually</h3>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="form-control w-full max-w-xs py-2"> <div className="form-control w-full max-w-xs py-2">
<label className="label"> <label className="label">
@@ -63,7 +63,7 @@ function ManualPositionModal({ showModal, botName, onClose }: ManualPositionModa
</div> </div>
<div className="modal-action"> <div className="modal-action">
<button type="submit" className="btn btn-primary">Open Position</button> <button type="submit" className="btn btn-primary">Create Signal</button>
<button type="button" className="btn" onClick={() => { reset(); onClose(); }}>Cancel</button> <button type="button" className="btn" onClick={() => { reset(); onClose(); }}>Cancel</button>
</div> </div>
</form> </form>

View File

@@ -18,15 +18,15 @@ const CustomMoneyManagement: React.FC<ICustomMoneyManagement> = ({
const { moneyManagement, setCustomMoneyManagement } = useCustomMoneyManagement() const { moneyManagement, setCustomMoneyManagement } = useCustomMoneyManagement()
const [leverage, setLeverage] = useState<number>(moneyManagement?.leverage || 1) const [leverage, setLeverage] = useState<number>(moneyManagement?.leverage || 1)
const [takeProfit, setTakeProfit] = useState<number>(moneyManagement?.takeProfit || 2) const [takeProfit, setTakeProfit] = useState<number>((moneyManagement?.takeProfit || 0.02) * 100) // Convert decimal to percentage
const [stopLoss, setStopLoss] = useState<number>(moneyManagement?.stopLoss || 1) const [stopLoss, setStopLoss] = useState<number>((moneyManagement?.stopLoss || 0.01) * 100) // Convert decimal to percentage
const handleCreateMoneyManagement = () => { const handleCreateMoneyManagement = () => {
const moneyManagement: MoneyManagement = { const moneyManagement: MoneyManagement = {
leverage, leverage,
name: 'custom', name: 'custom',
stopLoss, stopLoss: stopLoss / 100, // Convert percentage to decimal
takeProfit, takeProfit: takeProfit / 100, // Convert percentage to decimal
timeframe, timeframe,
} }
onCreateMoneyManagement(moneyManagement) onCreateMoneyManagement(moneyManagement)
@@ -37,6 +37,15 @@ const CustomMoneyManagement: React.FC<ICustomMoneyManagement> = ({
handleCreateMoneyManagement() handleCreateMoneyManagement()
}, [leverage, takeProfit, stopLoss]) }, [leverage, takeProfit, stopLoss])
// Update local state when global moneyManagement changes
useEffect(() => {
if (moneyManagement) {
setLeverage(moneyManagement.leverage)
setTakeProfit(moneyManagement.takeProfit * 100) // Convert decimal to percentage
setStopLoss(moneyManagement.stopLoss * 100) // Convert decimal to percentage
}
}, [moneyManagement])
return ( return (
<> <>
{showCustomMoneyManagement ? ( {showCustomMoneyManagement ? (

View File

@@ -142,7 +142,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
const { apiUrl } = useApiUrlStore(); const { apiUrl } = useApiUrlStore();
const { addBacktest } = useBacktestStore(); const { addBacktest } = useBacktestStore();
const { setCustomMoneyManagement: setGlobalCustomMoneyManagement } = useCustomMoneyManagement(); const { moneyManagement: globalCustomMoneyManagement, setCustomMoneyManagement: setGlobalCustomMoneyManagement } = useCustomMoneyManagement();
const { setCustomScenario: setGlobalCustomScenario } = useCustomScenario(); const { setCustomScenario: setGlobalCustomScenario } = useCustomScenario();
// API clients // API clients
@@ -620,7 +620,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
let moneyManagement: MoneyManagement | undefined = undefined; let moneyManagement: MoneyManagement | undefined = undefined;
if (showCustomMoneyManagement || (mode === 'createBot' && backtest)) { if (showCustomMoneyManagement || (mode === 'createBot' && backtest)) {
moneyManagement = customMoneyManagement; moneyManagement = globalCustomMoneyManagement || customMoneyManagement;
} else { } else {
const selectedMM = moneyManagements?.find(mm => mm.name === selectedMoneyManagement); const selectedMM = moneyManagements?.find(mm => mm.name === selectedMoneyManagement);
if (selectedMM) { if (selectedMM) {
@@ -712,7 +712,7 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
useForSignalFiltering: form.useForSignalFiltering ?? true, useForSignalFiltering: form.useForSignalFiltering ?? true,
useForDynamicStopLoss: form.useForDynamicStopLoss ?? true, useForDynamicStopLoss: form.useForDynamicStopLoss ?? true,
moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement, moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement,
moneyManagement: customMoneyManagement, moneyManagement: globalCustomMoneyManagement || customMoneyManagement,
flipPosition: form.flipPosition || false, flipPosition: form.flipPosition || false,
}; };

View File

@@ -1591,8 +1591,8 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<PaginatedResponseOfTradingBotResponse>(null as any); return Promise.resolve<PaginatedResponseOfTradingBotResponse>(null as any);
} }
bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise<Position> { bot_CreateManualSignal(request: CreateManualSignalRequest): Promise<LightSignal> {
let url_ = this.baseUrl + "/Bot/OpenPosition"; let url_ = this.baseUrl + "/Bot/CreateManualSignal";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(request); const content_ = JSON.stringify(request);
@@ -1609,17 +1609,17 @@ export class BotClient extends AuthorizedApiBase {
return this.transformOptions(options_).then(transformedOptions_ => { return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_); return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => { }).then((_response: Response) => {
return this.processBot_OpenPositionManually(_response); return this.processBot_CreateManualSignal(_response);
}); });
} }
protected processBot_OpenPositionManually(response: Response): Promise<Position> { protected processBot_CreateManualSignal(response: Response): Promise<LightSignal> {
const status = response.status; const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) { if (status === 200) {
return response.text().then((_responseText) => { return response.text().then((_responseText) => {
let result200: any = null; let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Position; result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as LightSignal;
return result200; return result200;
}); });
} else if (status !== 200 && status !== 204) { } else if (status !== 200 && status !== 204) {
@@ -1627,7 +1627,7 @@ export class BotClient extends AuthorizedApiBase {
return throwException("An unexpected server error occurred.", status, _responseText, _headers); return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}); });
} }
return Promise.resolve<Position>(null as any); return Promise.resolve<LightSignal>(null as any);
} }
bot_ClosePosition(request: ClosePositionRequest): Promise<Position> { bot_ClosePosition(request: ClosePositionRequest): Promise<Position> {
@@ -4309,7 +4309,7 @@ export enum BotSortableColumn {
AgentName = "AgentName", AgentName = "AgentName",
} }
export interface OpenPositionManuallyRequest { export interface CreateManualSignalRequest {
identifier?: string; identifier?: string;
direction?: TradeDirection; direction?: TradeDirection;
} }

View File

@@ -784,7 +784,7 @@ export enum BotSortableColumn {
AgentName = "AgentName", AgentName = "AgentName",
} }
export interface OpenPositionManuallyRequest { export interface CreateManualSignalRequest {
identifier?: string; identifier?: string;
direction?: TradeDirection; direction?: TradeDirection;
} }