Refactor pagination sorting parameters across multiple controllers and services to use the new SortDirection enum; update related API models and TypeScript definitions for consistency. Fix minor documentation and naming inconsistencies in the Bot and Data controllers.

This commit is contained in:
2025-12-14 20:23:26 +07:00
parent cb6b52ef4a
commit bcb00b9a86
30 changed files with 251 additions and 202 deletions

View File

@@ -538,7 +538,7 @@ public class BacktestController : BaseController
if (request.Config.Scenario != null)
{
// Convert ScenarioRequest to Scenario domain object
scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod)
scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LookbackPeriod)
{
User = user
};

View File

@@ -1,4 +1,4 @@
using Managing.Api.Models.Requests;
using Managing.Api.Models.Requests;
using Managing.Api.Models.Responses;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
@@ -430,7 +430,7 @@ public class BotController : BaseController
/// <param name="ticker">Filter by ticker (partial match, case-insensitive). If null, no ticker filtering is applied.</param>
/// <param name="agentName">Filter by agent name (partial match, case-insensitive). If null, no agent name filtering is applied.</param>
/// <param name="sortBy">Sort field. Valid values: "Name", "Ticker", "Status", "CreateDate", "StartupTime", "Pnl", "WinRate", "AgentName". Default is "CreateDate".</param>
/// <param name="sortDirection">Sort direction. Default is "Desc".</param>
/// <param name="sortDirection">Sort direction. Default is Desc.</param>
/// <returns>A paginated response containing trading bots</returns>
[HttpGet]
[Route("Paginated")]
@@ -442,7 +442,7 @@ public class BotController : BaseController
string? ticker = null,
string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc")
SortDirection sortDirection = SortDirection.Desc)
{
try
{
@@ -770,7 +770,7 @@ public class BotController : BaseController
if (request.Config.Scenario != null)
{
// Convert ScenarioRequest to Scenario domain object
scenarioForUpdate = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod)
scenarioForUpdate = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LookbackPeriod)
{
User = user
};
@@ -931,7 +931,7 @@ public class BotController : BaseController
if (request.Config.Scenario != null)
{
// Convert ScenarioRequest to Scenario domain object
scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod)
scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LookbackPeriod)
{
User = user
};

View File

@@ -1,4 +1,4 @@
using Managing.Api.Models.Requests;
using Managing.Api.Models.Requests;
using Managing.Api.Models.Responses;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
@@ -561,7 +561,7 @@ public class DataController : ControllerBase
// Use caching for position data in UI context (not critical trading operations)
var cacheKey = $"positions_{strategy.Identifier}";
var cachedPositions = _cacheService.GetValue<List<Position>>(cacheKey);
List<Position> positions;
if (cachedPositions != null)
{
@@ -571,7 +571,7 @@ public class DataController : ControllerBase
{
// Fetch positions associated with this bot using the provided trading service
positions = (await tradingService.GetPositionsByInitiatorIdentifierAsync(strategy.Identifier)).ToList();
// Cache positions for 2 minutes for UI display purposes
_cacheService.SaveValue(cacheKey, positions, TimeSpan.FromMinutes(2));
}
@@ -619,9 +619,9 @@ public class DataController : ControllerBase
NetPnL = strategy.NetPnL,
ROIPercentage = strategy.Roi,
Runtime = strategy.Status == BotStatus.Running ? strategy.LastStartTime : null,
LastStartTime = strategy.LastStartTime,
LastStopTime = strategy.LastStopTime,
AccumulatedRunTimeSeconds = strategy.AccumulatedRunTimeSeconds,
LastStartTime = strategy.LastStartTime,
LastStopTime = strategy.LastStopTime,
AccumulatedRunTimeSeconds = strategy.AccumulatedRunTimeSeconds,
TotalRuntimeSeconds = strategy.GetTotalRuntimeSeconds(),
WinRate = winRate,
TotalVolumeTraded = totalVolume,
@@ -715,7 +715,9 @@ public class DataController : ControllerBase
var showOnlyProfitable = _configuration.GetValue<bool>("showOnlyProfitable", false);
// Get paginated results from database
var command = new GetPaginatedAgentSummariesCommand(page, pageSize, sortBy, sortOrder, agentNamesList, showOnlyProfitable);
var command =
new GetPaginatedAgentSummariesCommand(page, pageSize, sortBy, sortOrder, agentNamesList,
showOnlyProfitable);
var result = await _mediator.Send(command);
var agentSummaries = result.Results;
var totalCount = result.TotalCount;
@@ -819,7 +821,7 @@ public class DataController : ControllerBase
/// <returns>A domain Scenario object.</returns>
private Scenario MapScenarioRequestToScenario(ScenarioRequest scenarioRequest)
{
var scenario = new Scenario(scenarioRequest.Name, scenarioRequest.LoopbackPeriod);
var scenario = new Scenario(scenarioRequest.Name, scenarioRequest.LookbackPeriod);
foreach (var indicatorRequest in scenarioRequest.Indicators)
{
@@ -907,7 +909,7 @@ public class DataController : ControllerBase
/// <param name="ticker">Filter by ticker (partial match, case-insensitive)</param>
/// <param name="agentName">Filter by agent name (partial match, case-insensitive)</param>
/// <param name="sortBy">Sort field (defaults to CreateDate)</param>
/// <param name="sortDirection">Sort direction - "Asc" or "Desc" (defaults to "Desc")</param>
/// <param name="sortDirection">Sort direction - Asc or Desc (defaults to Desc)</param>
/// <returns>A paginated list of strategies excluding Saved status bots</returns>
[HttpGet("GetStrategiesPaginated")]
public async Task<ActionResult<PaginatedResponse<TradingBotResponse>>> GetStrategiesPaginated(
@@ -917,7 +919,7 @@ public class DataController : ControllerBase
string? ticker = null,
string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc")
SortDirection sortDirection = SortDirection.Desc)
{
// Validate pagination parameters
if (pageNumber < 1)
@@ -930,11 +932,6 @@ public class DataController : ControllerBase
return BadRequest("Page size must be between 1 and 100");
}
// Validate sort direction
if (sortDirection != "Asc" && sortDirection != "Desc")
{
return BadRequest("Sort direction must be 'Asc' or 'Desc'");
}
try
{

View File

@@ -191,7 +191,7 @@ public class ScenarioController : BaseController
return new ScenarioViewModel
{
Name = scenario.Name,
LoopbackPeriod = scenario.LoopbackPeriod,
LoopbackPeriod = scenario.LookbackPeriod,
UserName = scenario.User?.Name,
Indicators = scenario.Indicators?.Select(MapToIndicatorViewModel).ToList() ?? new List<IndicatorViewModel>()
};

View File

@@ -49,7 +49,7 @@ public class GetBotsPaginatedRequest
public BotSortableColumn SortBy { get; set; } = BotSortableColumn.CreateDate;
/// <summary>
/// Sort direction. Default is "Desc" (descending).
/// Sort direction. Default is Desc (descending).
/// </summary>
public string SortDirection { get; set; } = "Desc";
public SortDirection SortDirection { get; set; } = SortDirection.Desc;
}

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Managing.Common;
namespace Managing.Api.Models.Responses
@@ -10,41 +11,49 @@ namespace Managing.Api.Models.Responses
/// <summary>
/// Name of the deployed strategy
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// Current state of the strategy (RUNNING, STOPPED, UNUSED)
/// </summary>
[Required]
public Enums.BotStatus State { get; set; }
/// <summary>
/// Total profit or loss generated by the strategy in USD (gross, before fees)
/// </summary>
[Required]
public decimal PnL { get; set; }
/// <summary>
/// Net profit or loss generated by the strategy in USD (after fees)
/// </summary>
[Required]
public decimal NetPnL { get; set; }
/// <summary>
/// Return on investment percentage
/// </summary>
[Required]
public decimal ROIPercentage { get; set; }
/// <summary>
/// Date and time when the strategy was started (only present when running, for live ticker)
/// </summary>
[Required]
public DateTime? Runtime { get; set; }
/// <summary>
/// Total accumulated runtime in seconds (including current session if running)
/// </summary>
[Required]
public long TotalRuntimeSeconds { get; set; }
/// <summary>
/// Time when the current or last session started
/// </summary>
[Required]
public DateTime? LastStartTime { get; set; }
/// <summary>
@@ -55,42 +64,49 @@ namespace Managing.Api.Models.Responses
/// <summary>
/// Total accumulated runtime across all past sessions (seconds)
/// </summary>
[Required]
public long AccumulatedRunTimeSeconds { get; set; }
/// <summary>
/// Average percentage of successful trades
/// </summary>
[Required]
public int WinRate { get; set; }
/// <summary>
/// Total trading volume for all trades
/// </summary>
[Required]
public decimal TotalVolumeTraded { get; set; }
/// <summary>
/// Trading volume in the last 24 hours
/// </summary>
[Required]
public decimal VolumeLast24H { get; set; }
/// <summary>
/// Number of winning trades
/// </summary>
[Required]
public int Wins { get; set; }
/// <summary>
/// Number of losing trades
/// </summary>
[Required]
public int Losses { get; set; }
/// <summary>
/// Dictionary of all positions executed by this strategy, keyed by position identifier
/// </summary>
[Required]
public List<PositionViewModel> Positions { get; set; } = new List<PositionViewModel>();
public Guid Identifier { get; set; }
[Required] public Guid Identifier { get; set; }
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new Dictionary<DateTime, decimal>();
public Enums.Ticker Ticker { get; set; }
[Required] public Enums.Ticker Ticker { get; set; }
/// <summary>
/// The agent name of the master bot's owner (for copy trading bots)

View File

@@ -37,7 +37,7 @@ public interface IBotRepository
string? ticker = null,
string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc",
SortDirection sortDirection = SortDirection.Desc,
bool showOnlyProfitable = false);
/// <summary>

View File

@@ -158,7 +158,7 @@ public class BacktestTests : BaseTests
var scenario = new Scenario("ETH_BacktestScenario");
var rsiDivIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
scenario.Indicators = new List<IndicatorBase> { (IndicatorBase)rsiDivIndicator };
scenario.LoopbackPeriod = 15;
scenario.LookbackPeriod = 15;
var config = new TradingBotConfig
{

View File

@@ -47,7 +47,7 @@ public interface IBotService
/// <param name="ticker">Filter by ticker (partial match, case-insensitive)</param>
/// <param name="agentName">Filter by agent name (partial match, case-insensitive)</param>
/// <param name="sortBy">Sort field</param>
/// <param name="sortDirection">Sort direction ("Asc" or "Desc")</param>
/// <param name="sortDirection">Sort direction</param>
/// <param name="showOnlyProfitable">Whether to show only profitable bots (ROI > 0)</param>
/// <returns>Tuple containing the bots for the current page and total count</returns>
Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
@@ -58,7 +58,7 @@ public interface IBotService
string? ticker = null,
string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc",
SortDirection sortDirection = SortDirection.Desc,
bool showOnlyProfitable = false);
/// <summary>

View File

@@ -7,7 +7,7 @@ namespace Managing.Application.Abstractions
{
public interface IScenarioService
{
Task<Scenario> CreateScenario(string name, List<string> strategies, int? loopbackPeriod = 1);
Task<Scenario> CreateScenario(string name, List<string> strategies, int loopbackPeriod = 1);
Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync();
Task<bool> UpdateScenario(string name, List<string> strategies, int? loopbackPeriod);

View File

@@ -151,7 +151,7 @@ public class JobService
if (backtestRequest.Config.Scenario != null)
{
var sReq = backtestRequest.Config.Scenario;
scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod)
scenario = new LightScenario(sReq.Name, sReq.LookbackPeriod)
{
Indicators = sReq.Indicators?.Select(ind => new LightIndicator(ind.Name, ind.Type)
{

View File

@@ -176,7 +176,7 @@ public class BacktestFuturesBot : TradingBotBase, ITradingBot
throw new ArgumentNullException(nameof(Config.Scenario), "Config.Scenario cannot be null");
// Use TradingBox.GetSignal for backtest with pre-calculated indicators
var backtestSignal = TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod,
var backtestSignal = TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LookbackPeriod,
preCalculatedIndicatorValues);
if (backtestSignal == null) return;

View File

@@ -180,7 +180,7 @@ public class BacktestSpotBot : TradingBotBase, ITradingBot
throw new ArgumentNullException(nameof(Config.Scenario), "Config.Scenario cannot be null");
// Use TradingBox.GetSignal for backtest with pre-calculated indicators
var backtestSignal = TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod,
var backtestSignal = TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LookbackPeriod,
preCalculatedIndicatorValues);
if (backtestSignal == null) return;

View File

@@ -487,7 +487,7 @@ namespace Managing.Application.ManageBot
string? ticker = null,
string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc",
SortDirection sortDirection = SortDirection.Desc,
bool showOnlyProfitable = false)
{
return await ServiceScopeHelpers.WithScopedService<IBotRepository, (IEnumerable<Bot> Bots, int TotalCount)>(

View File

@@ -68,7 +68,7 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
// Get candles as ordered List (already ordered by date from CandleStoreGrain)
var candlesList = await GetCandlesAsync(tradingExchanges, config);
if (candlesList.Count == 0)
{
_logger.LogWarning($"No candles available for {config.Ticker} for {config.Name}");
@@ -88,7 +88,7 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
candlesList,
config.Scenario,
previousSignals,
config.Scenario?.LoopbackPeriod ?? 1);
config.Scenario?.LookbackPeriod ?? 1);
if (signal != null && signal.Date > candle.Date)
{

View File

@@ -20,7 +20,7 @@ namespace Managing.Application.Scenarios
_tradingService = tradingService;
}
public async Task<Scenario> CreateScenario(string name, List<string> strategies, int? loopbackPeriod = 1)
public async Task<Scenario> CreateScenario(string name, List<string> strategies, int loopbackPeriod = 1)
{
var scenario = new Scenario(name, loopbackPeriod);
@@ -77,7 +77,7 @@ namespace Managing.Application.Scenarios
scenario.AddIndicator(await _tradingService.GetIndicatorByNameAsync(strategy));
}
scenario.LoopbackPeriod = loopbackPeriod ?? 1;
scenario.LookbackPeriod = loopbackPeriod ?? 1;
await _tradingService.UpdateScenarioAsync(scenario);
return true;
}
@@ -262,7 +262,7 @@ namespace Managing.Application.Scenarios
}
scenario.Indicators.Clear();
scenario.LoopbackPeriod = loopbackPeriod ?? 1;
scenario.LookbackPeriod = loopbackPeriod ?? 1;
foreach (var strategyName in strategies)
{

View File

@@ -225,7 +225,7 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
if (runBacktestRequest.Config.Scenario != null)
{
var sReq = runBacktestRequest.Config.Scenario;
scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod)
scenario = new LightScenario(sReq.Name, sReq.LookbackPeriod)
{
Indicators = sReq.Indicators?.Select(i => new LightIndicator(i.Name, i.Type)
{

View File

@@ -1,4 +1,4 @@
namespace Managing.Common;
namespace Managing.Common;
public static class Enums
{
@@ -101,6 +101,15 @@ public static class Enums
AgentName
}
/// <summary>
/// Sort direction for pagination endpoints
/// </summary>
public enum SortDirection
{
Asc,
Desc
}
public enum SignalStatus
{
WaitingForPosition,

View File

@@ -22,5 +22,5 @@ public class ScenarioRequest
/// <summary>
/// The loopback period for the scenario
/// </summary>
public int? LoopbackPeriod { get; set; }
}
public int LookbackPeriod { get; set; }
}

View File

@@ -45,6 +45,7 @@ public class TradingBotConfig
/// Orleans-friendly version without FixedSizeQueue and User properties.
/// </summary>
[Id(12)]
[Required]
public LightScenario Scenario { get; set; }
/// <summary>
@@ -104,6 +105,7 @@ public class TradingBotConfig
/// </summary>
[Id(20)]
public bool UseForDynamicStopLoss { get; set; } = true;
/// <summary>
/// Parameter to indicate if the bot is for copy trading
/// </summary>

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Managing.Domain.Scenarios;
using Orleans;
using static Managing.Common.Enums;
@@ -18,11 +19,11 @@ public class LightIndicator
SignalType = ScenarioHelpers.GetSignalType(type);
}
[Id(0)] public string Name { get; set; }
[Id(0)] [Required] public string Name { get; set; }
[Id(1)] public IndicatorType Type { get; set; }
[Id(1)] [Required] public IndicatorType Type { get; set; }
[Id(2)] public SignalType SignalType { get; set; }
[Id(2)] [Required] public SignalType SignalType { get; set; }
[Id(3)] public int MinimumHistory { get; set; }

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Managing.Domain.Strategies;
using Orleans;
@@ -10,25 +11,25 @@ namespace Managing.Domain.Scenarios;
[GenerateSerializer]
public class LightScenario
{
public LightScenario(string name, int? loopbackPeriod = 1)
public LightScenario(string name, int lookbackPeriod = 1)
{
Name = name;
Indicators = new List<LightIndicator>();
LoopbackPeriod = loopbackPeriod;
LookbackPeriod = lookbackPeriod;
}
[Id(0)] public string Name { get; set; }
[Id(1)] public List<LightIndicator> Indicators { get; set; }
[Id(2)] public int? LoopbackPeriod { get; set; }
[Id(2)] [Required] public int LookbackPeriod { get; set; }
/// <summary>
/// Converts a full Scenario to a LightScenario
/// </summary>
public static LightScenario FromScenario(Scenario scenario)
{
var lightScenario = new LightScenario(scenario.Name, scenario.LoopbackPeriod)
var lightScenario = new LightScenario(scenario.Name, scenario.LookbackPeriod)
{
Indicators = scenario.Indicators?.Select(ScenarioHelpers.BaseToLight).ToList() ??
new List<LightIndicator>()
@@ -41,7 +42,7 @@ public class LightScenario
/// </summary>
public Scenario ToScenario()
{
var scenario = new Scenario(Name, LoopbackPeriod)
var scenario = new Scenario(Name, LookbackPeriod)
{
Indicators = Indicators?.Select(li => li.LightToBase()).ToList()
};

View File

@@ -7,18 +7,18 @@ namespace Managing.Domain.Scenarios
[GenerateSerializer]
public class Scenario
{
public Scenario(string name, int? loopbackPeriod = 1)
public Scenario(string name, int lookbackPeriod = 1)
{
Name = name;
Indicators = new List<IndicatorBase>();
LoopbackPeriod = loopbackPeriod;
LookbackPeriod = lookbackPeriod;
}
[Id(0)] public string Name { get; set; }
[Id(1)] public List<IndicatorBase> Indicators { get; set; }
[Id(2)] public int? LoopbackPeriod { get; set; }
[Id(2)] public int LookbackPeriod { get; set; }
[Id(3)] public User User { get; set; }

View File

@@ -206,7 +206,7 @@ public class PostgreSqlBotRepository : IBotRepository
string? ticker = null,
string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc",
SortDirection sortDirection = SortDirection.Desc,
bool showOnlyProfitable = false)
{
// Build the query with filters
@@ -249,33 +249,33 @@ public class PostgreSqlBotRepository : IBotRepository
// Apply sorting
query = sortBy switch
{
BotSortableColumn.Name => sortDirection.ToLower() == "asc"
BotSortableColumn.Name => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.Name)
: query.OrderByDescending(b => b.Name),
BotSortableColumn.Ticker => sortDirection.ToLower() == "asc"
BotSortableColumn.Ticker => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.Ticker)
: query.OrderByDescending(b => b.Ticker),
BotSortableColumn.Status => sortDirection.ToLower() == "asc"
BotSortableColumn.Status => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.Status)
: query.OrderByDescending(b => b.Status),
BotSortableColumn.StartupTime => sortDirection.ToLower() == "asc"
BotSortableColumn.StartupTime => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.StartupTime)
: query.OrderByDescending(b => b.StartupTime),
BotSortableColumn.Roi => sortDirection.ToLower() == "asc"
BotSortableColumn.Roi => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.Roi)
: query.OrderByDescending(b => b.Roi),
BotSortableColumn.Pnl => sortDirection.ToLower() == "asc"
BotSortableColumn.Pnl => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.Pnl)
: query.OrderByDescending(b => b.Pnl),
BotSortableColumn.WinRate => sortDirection.ToLower() == "asc"
BotSortableColumn.WinRate => sortDirection == SortDirection.Asc
? query.OrderBy(b =>
(b.TradeWins + b.TradeLosses) > 0 ? (double)b.TradeWins / (b.TradeWins + b.TradeLosses) : 0)
: query.OrderByDescending(b =>
(b.TradeWins + b.TradeLosses) > 0 ? (double)b.TradeWins / (b.TradeWins + b.TradeLosses) : 0),
BotSortableColumn.AgentName => sortDirection.ToLower() == "asc"
BotSortableColumn.AgentName => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.User.AgentName)
: query.OrderByDescending(b => b.User.AgentName),
_ => sortDirection.ToLower() == "asc"
_ => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.CreateDate)
: query.OrderByDescending(b => b.CreateDate)
};

View File

@@ -360,7 +360,9 @@ public static class PostgreSqlMappers
Duration = backtest.EndDate - backtest.StartDate,
MoneyManagementJson = JsonConvert.SerializeObject(backtest.Config?.MoneyManagement, jsonSettings),
UserId = backtest.User?.Id ?? 0,
StatisticsJson = backtest.Statistics != null ? JsonConvert.SerializeObject(backtest.Statistics, jsonSettings) : null,
StatisticsJson = backtest.Statistics != null
? JsonConvert.SerializeObject(backtest.Statistics, jsonSettings)
: null,
SharpeRatio = backtest.Statistics?.SharpeRatio ?? 0m,
MaxDrawdown = backtest.Statistics?.MaxDrawdown ?? 0m,
MaxDrawdownRecoveryTime = backtest.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero,
@@ -503,7 +505,7 @@ public static class PostgreSqlMappers
return new ScenarioEntity
{
Name = scenario.Name,
LoopbackPeriod = scenario.LoopbackPeriod ?? 1,
LoopbackPeriod = scenario.LookbackPeriod,
UserId = scenario.User?.Id ?? 0
};
}

View File

@@ -13,7 +13,8 @@ namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlTradingRepository : BaseRepositoryWithLogging, ITradingRepository
{
public PostgreSqlTradingRepository(ManagingDbContext context, ILogger<SqlQueryLogger> logger, SentrySqlMonitoringService sentryMonitoringService)
public PostgreSqlTradingRepository(ManagingDbContext context, ILogger<SqlQueryLogger> logger,
SentrySqlMonitoringService sentryMonitoringService)
: base(context, logger, sentryMonitoringService)
{
}
@@ -154,7 +155,7 @@ public class PostgreSqlTradingRepository : BaseRepositoryWithLogging, ITradingRe
if (entity != null)
{
entity.LoopbackPeriod = scenario.LoopbackPeriod ?? 1;
entity.LoopbackPeriod = scenario.LookbackPeriod;
entity.UserId = scenario.User?.Id ?? 0;
entity.UpdatedAt = DateTime.UtcNow;
@@ -408,62 +409,63 @@ public class PostgreSqlTradingRepository : BaseRepositoryWithLogging, ITradingRe
public async Task UpdatePositionAsync(Position position)
{
await ExecuteWithLoggingAsync(async () =>
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
var entity = _context.Positions
.AsTracking()
.Include(p => p.OpenTrade)
.Include(p => p.StopLossTrade)
.Include(p => p.TakeProfit1Trade)
.Include(p => p.TakeProfit2Trade)
.FirstOrDefault(p => p.Identifier == position.Identifier);
if (entity != null)
try
{
entity.ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0;
entity.NetPnL = position.ProfitAndLoss?.Net ?? 0;
entity.UiFees = position.UiFees;
// entity.OriginDirection = position.OriginDirection;
entity.GasFees = position.GasFees;
entity.Status = position.Status;
entity.MoneyManagementJson = position.MoneyManagement != null
? JsonConvert.SerializeObject(position.MoneyManagement)
: null;
entity.UpdatedAt = DateTime.UtcNow;
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
// Update related trades directly through the position's trade references
// This ensures we're updating the correct trade records for this specific position
if (position.Open != null && entity.OpenTrade != null)
var entity = _context.Positions
.AsTracking()
.Include(p => p.OpenTrade)
.Include(p => p.StopLossTrade)
.Include(p => p.TakeProfit1Trade)
.Include(p => p.TakeProfit2Trade)
.FirstOrDefault(p => p.Identifier == position.Identifier);
if (entity != null)
{
UpdateTradeEntity(entity.OpenTrade, position.Open);
}
entity.ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0;
entity.NetPnL = position.ProfitAndLoss?.Net ?? 0;
entity.UiFees = position.UiFees;
// entity.OriginDirection = position.OriginDirection;
entity.GasFees = position.GasFees;
entity.Status = position.Status;
entity.MoneyManagementJson = position.MoneyManagement != null
? JsonConvert.SerializeObject(position.MoneyManagement)
: null;
entity.UpdatedAt = DateTime.UtcNow;
if (position.StopLoss != null && entity.StopLossTrade != null)
{
UpdateTradeEntity(entity.StopLossTrade, position.StopLoss);
}
// Update related trades directly through the position's trade references
// This ensures we're updating the correct trade records for this specific position
if (position.Open != null && entity.OpenTrade != null)
{
UpdateTradeEntity(entity.OpenTrade, position.Open);
}
if (position.TakeProfit1 != null && entity.TakeProfit1Trade != null)
{
UpdateTradeEntity(entity.TakeProfit1Trade, position.TakeProfit1);
}
if (position.StopLoss != null && entity.StopLossTrade != null)
{
UpdateTradeEntity(entity.StopLossTrade, position.StopLoss);
}
if (position.TakeProfit2 != null && entity.TakeProfit2Trade != null)
{
UpdateTradeEntity(entity.TakeProfit2Trade, position.TakeProfit2);
}
if (position.TakeProfit1 != null && entity.TakeProfit1Trade != null)
{
UpdateTradeEntity(entity.TakeProfit1Trade, position.TakeProfit1);
}
await _context.SaveChangesAsync();
if (position.TakeProfit2 != null && entity.TakeProfit2Trade != null)
{
UpdateTradeEntity(entity.TakeProfit2Trade, position.TakeProfit2);
}
await _context.SaveChangesAsync();
}
}
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(UpdatePositionAsync), ("positionIdentifier", position.Identifier), ("positionStatus", position.Status));
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(UpdatePositionAsync), ("positionIdentifier", position.Identifier),
("positionStatus", position.Status));
}
/// <summary>
@@ -474,7 +476,7 @@ public class PostgreSqlTradingRepository : BaseRepositoryWithLogging, ITradingRe
{
// Only update the date if the trade status is changing from Requested to Filled
// This prevents overwriting dates for trades that are already filled
if (entity.Status != trade.Status)
if (entity.Status != trade.Status)
{
entity.Date = trade.Date;
}

View File

@@ -355,7 +355,7 @@ export interface TradingBotConfig {
flipPosition: boolean;
name: string;
riskManagement?: RiskManagement | null;
scenario?: LightScenario | null;
scenario: LightScenario;
scenarioName?: string | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
@@ -420,13 +420,13 @@ export enum RiskToleranceLevel {
export interface LightScenario {
name?: string | null;
indicators?: LightIndicator[] | null;
loopbackPeriod?: number | null;
lookbackPeriod: number;
}
export interface LightIndicator {
name?: string | null;
type?: IndicatorType;
signalType?: SignalType;
name: string;
type: IndicatorType;
signalType: SignalType;
minimumHistory?: number;
period?: number | null;
fastPeriods?: number | null;
@@ -728,7 +728,7 @@ export interface TradingBotConfigRequest {
export interface ScenarioRequest {
name: string;
indicators: IndicatorRequest[];
loopbackPeriod?: number | null;
lookbackPeriod?: number;
}
export interface IndicatorRequest {
@@ -1014,6 +1014,11 @@ export enum BotSortableColumn {
AgentName = "AgentName",
}
export enum SortDirection {
Asc = "Asc",
Desc = "Desc",
}
export interface CreateManualSignalRequest {
identifier?: string;
direction?: TradeDirection;
@@ -1052,7 +1057,7 @@ export interface Spotlight {
export interface Scenario {
name?: string | null;
indicators?: IndicatorBase[] | null;
loopbackPeriod?: number | null;
lookbackPeriod?: number;
user?: User | null;
}
@@ -1231,25 +1236,25 @@ export interface StrategyRoiPerformance {
}
export interface UserStrategyDetailsViewModel {
name?: string | null;
state?: BotStatus;
pnL?: number;
netPnL?: number;
roiPercentage?: number;
runtime?: Date | null;
totalRuntimeSeconds?: number;
lastStartTime?: Date | null;
name: string;
state: BotStatus;
pnL: number;
netPnL: number;
roiPercentage: number;
runtime: Date;
totalRuntimeSeconds: number;
lastStartTime: Date;
lastStopTime?: Date | null;
accumulatedRunTimeSeconds?: number;
winRate?: number;
totalVolumeTraded?: number;
volumeLast24H?: number;
wins?: number;
losses?: number;
positions?: PositionViewModel[] | null;
identifier?: string;
accumulatedRunTimeSeconds: number;
winRate: number;
totalVolumeTraded: number;
volumeLast24H: number;
wins: number;
losses: number;
positions: PositionViewModel[];
identifier: string;
walletBalances?: { [key: string]: number; } | null;
ticker?: Ticker;
ticker: Ticker;
masterAgentName?: string | null;
}

View File

@@ -1917,7 +1917,7 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<TradingBotResponse[]>(null as any);
}
bot_GetBotsPaginated(pageNumber: number | undefined, pageSize: number | undefined, status: BotStatus | null | undefined, name: string | null | undefined, ticker: string | null | undefined, agentName: string | null | undefined, sortBy: BotSortableColumn | undefined, sortDirection: string | null | undefined): Promise<PaginatedResponseOfTradingBotResponse> {
bot_GetBotsPaginated(pageNumber: number | undefined, pageSize: number | undefined, status: BotStatus | null | undefined, name: string | null | undefined, ticker: string | null | undefined, agentName: string | null | undefined, sortBy: BotSortableColumn | undefined, sortDirection: SortDirection | undefined): Promise<PaginatedResponseOfTradingBotResponse> {
let url_ = this.baseUrl + "/Bot/Paginated?";
if (pageNumber === null)
throw new Error("The parameter 'pageNumber' cannot be null.");
@@ -1939,7 +1939,9 @@ export class BotClient extends AuthorizedApiBase {
throw new Error("The parameter 'sortBy' cannot be null.");
else if (sortBy !== undefined)
url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&";
if (sortDirection !== undefined && sortDirection !== null)
if (sortDirection === null)
throw new Error("The parameter 'sortDirection' cannot be null.");
else if (sortDirection !== undefined)
url_ += "sortDirection=" + encodeURIComponent("" + sortDirection) + "&";
url_ = url_.replace(/[?&]$/, "");
@@ -2599,7 +2601,7 @@ export class DataClient extends AuthorizedApiBase {
return Promise.resolve<string[]>(null as any);
}
data_GetStrategiesPaginated(pageNumber: number | undefined, pageSize: number | undefined, name: string | null | undefined, ticker: string | null | undefined, agentName: string | null | undefined, sortBy: BotSortableColumn | undefined, sortDirection: string | null | undefined): Promise<PaginatedResponseOfTradingBotResponse> {
data_GetStrategiesPaginated(pageNumber: number | undefined, pageSize: number | undefined, name: string | null | undefined, ticker: string | null | undefined, agentName: string | null | undefined, sortBy: BotSortableColumn | undefined, sortDirection: SortDirection | undefined): Promise<PaginatedResponseOfTradingBotResponse> {
let url_ = this.baseUrl + "/Data/GetStrategiesPaginated?";
if (pageNumber === null)
throw new Error("The parameter 'pageNumber' cannot be null.");
@@ -2619,7 +2621,9 @@ export class DataClient extends AuthorizedApiBase {
throw new Error("The parameter 'sortBy' cannot be null.");
else if (sortBy !== undefined)
url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&";
if (sortDirection !== undefined && sortDirection !== null)
if (sortDirection === null)
throw new Error("The parameter 'sortDirection' cannot be null.");
else if (sortDirection !== undefined)
url_ += "sortDirection=" + encodeURIComponent("" + sortDirection) + "&";
url_ = url_.replace(/[?&]$/, "");
@@ -4854,7 +4858,7 @@ export interface TradingBotConfig {
flipPosition: boolean;
name: string;
riskManagement?: RiskManagement | null;
scenario?: LightScenario | null;
scenario: LightScenario;
scenarioName?: string | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
@@ -4919,13 +4923,13 @@ export enum RiskToleranceLevel {
export interface LightScenario {
name?: string | null;
indicators?: LightIndicator[] | null;
loopbackPeriod?: number | null;
lookbackPeriod: number;
}
export interface LightIndicator {
name?: string | null;
type?: IndicatorType;
signalType?: SignalType;
name: string;
type: IndicatorType;
signalType: SignalType;
minimumHistory?: number;
period?: number | null;
fastPeriods?: number | null;
@@ -5227,7 +5231,7 @@ export interface TradingBotConfigRequest {
export interface ScenarioRequest {
name: string;
indicators: IndicatorRequest[];
loopbackPeriod?: number | null;
lookbackPeriod?: number;
}
export interface IndicatorRequest {
@@ -5513,6 +5517,11 @@ export enum BotSortableColumn {
AgentName = "AgentName",
}
export enum SortDirection {
Asc = "Asc",
Desc = "Desc",
}
export interface CreateManualSignalRequest {
identifier?: string;
direction?: TradeDirection;
@@ -5551,7 +5560,7 @@ export interface Spotlight {
export interface Scenario {
name?: string | null;
indicators?: IndicatorBase[] | null;
loopbackPeriod?: number | null;
lookbackPeriod?: number;
user?: User | null;
}
@@ -5730,25 +5739,25 @@ export interface StrategyRoiPerformance {
}
export interface UserStrategyDetailsViewModel {
name?: string | null;
state?: BotStatus;
pnL?: number;
netPnL?: number;
roiPercentage?: number;
runtime?: Date | null;
totalRuntimeSeconds?: number;
lastStartTime?: Date | null;
name: string;
state: BotStatus;
pnL: number;
netPnL: number;
roiPercentage: number;
runtime: Date;
totalRuntimeSeconds: number;
lastStartTime: Date;
lastStopTime?: Date | null;
accumulatedRunTimeSeconds?: number;
winRate?: number;
totalVolumeTraded?: number;
volumeLast24H?: number;
wins?: number;
losses?: number;
positions?: PositionViewModel[] | null;
identifier?: string;
accumulatedRunTimeSeconds: number;
winRate: number;
totalVolumeTraded: number;
volumeLast24H: number;
wins: number;
losses: number;
positions: PositionViewModel[];
identifier: string;
walletBalances?: { [key: string]: number; } | null;
ticker?: Ticker;
ticker: Ticker;
masterAgentName?: string | null;
}

View File

@@ -355,7 +355,7 @@ export interface TradingBotConfig {
flipPosition: boolean;
name: string;
riskManagement?: RiskManagement | null;
scenario?: LightScenario | null;
scenario: LightScenario;
scenarioName?: string | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
@@ -420,13 +420,13 @@ export enum RiskToleranceLevel {
export interface LightScenario {
name?: string | null;
indicators?: LightIndicator[] | null;
loopbackPeriod?: number | null;
lookbackPeriod: number;
}
export interface LightIndicator {
name?: string | null;
type?: IndicatorType;
signalType?: SignalType;
name: string;
type: IndicatorType;
signalType: SignalType;
minimumHistory?: number;
period?: number | null;
fastPeriods?: number | null;
@@ -728,7 +728,7 @@ export interface TradingBotConfigRequest {
export interface ScenarioRequest {
name: string;
indicators: IndicatorRequest[];
loopbackPeriod?: number | null;
lookbackPeriod?: number;
}
export interface IndicatorRequest {
@@ -1014,6 +1014,11 @@ export enum BotSortableColumn {
AgentName = "AgentName",
}
export enum SortDirection {
Asc = "Asc",
Desc = "Desc",
}
export interface CreateManualSignalRequest {
identifier?: string;
direction?: TradeDirection;
@@ -1052,7 +1057,7 @@ export interface Spotlight {
export interface Scenario {
name?: string | null;
indicators?: IndicatorBase[] | null;
loopbackPeriod?: number | null;
lookbackPeriod?: number;
user?: User | null;
}
@@ -1231,25 +1236,25 @@ export interface StrategyRoiPerformance {
}
export interface UserStrategyDetailsViewModel {
name?: string | null;
state?: BotStatus;
pnL?: number;
netPnL?: number;
roiPercentage?: number;
runtime?: Date | null;
totalRuntimeSeconds?: number;
lastStartTime?: Date | null;
name: string;
state: BotStatus;
pnL: number;
netPnL: number;
roiPercentage: number;
runtime: Date;
totalRuntimeSeconds: number;
lastStartTime: Date;
lastStopTime?: Date | null;
accumulatedRunTimeSeconds?: number;
winRate?: number;
totalVolumeTraded?: number;
volumeLast24H?: number;
wins?: number;
losses?: number;
positions?: PositionViewModel[] | null;
identifier?: string;
accumulatedRunTimeSeconds: number;
winRate: number;
totalVolumeTraded: number;
volumeLast24H: number;
wins: number;
losses: number;
positions: PositionViewModel[];
identifier: string;
walletBalances?: { [key: string]: number; } | null;
ticker?: Ticker;
ticker: Ticker;
masterAgentName?: string | null;
}

View File

@@ -112,7 +112,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
var scenario = new Scenario("ETH_BacktestScenario");
var rsiDivIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
scenario.Indicators = new List<IndicatorBase> { (IndicatorBase)rsiDivIndicator };
scenario.LoopbackPeriod = 15;
scenario.LookbackPeriod = 15;
var config = new TradingBotConfig
{
@@ -207,7 +207,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
var scenario = new Scenario("ETH_BacktestScenario");
var rsiDivIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
scenario.Indicators = new List<IndicatorBase> { (IndicatorBase)rsiDivIndicator };
scenario.LoopbackPeriod = 15;
scenario.LookbackPeriod = 15;
var config = new TradingBotConfig
{
@@ -297,7 +297,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
var scenario = new Scenario("ETH_BacktestScenario");
var rsiDivIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
scenario.Indicators = new List<IndicatorBase> { (IndicatorBase)rsiDivIndicator };
scenario.LoopbackPeriod = 15;
scenario.LookbackPeriod = 15;
var config = new TradingBotConfig
{
@@ -387,7 +387,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
var emaCrossIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.EmaCross, "EmaCross", period: 21);
scenario.Indicators = new List<IndicatorBase>
{ (IndicatorBase)rsiDivIndicator, (IndicatorBase)emaCrossIndicator };
scenario.LoopbackPeriod = 15; // 15 minutes loopback period as requested
scenario.LookbackPeriod = 15; // 15 minutes loopback period as requested
var config = new TradingBotConfig
{
@@ -488,7 +488,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
Console.WriteLine($"📈 Win Rate: {result.WinRate}% (Expected: {expectedWinRatePercent}%)");
Console.WriteLine($"📈 Growth: {result.GrowthPercentage:F2}% (Expected: {expectedGrowthPercentage:F2}%)");
Console.WriteLine(
$"🎭 Scenario: {scenario.Name} ({scenario.Indicators.Count} indicators, LoopbackPeriod: {scenario.LoopbackPeriod})");
$"🎭 Scenario: {scenario.Name} ({scenario.Indicators.Count} indicators, LoopbackPeriod: {scenario.LookbackPeriod})");
// Performance assertion - should be reasonably fast even with 2 indicators
Assert.True(candlesPerSecond > 200,
@@ -510,7 +510,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
var scenario = new Scenario("ETH_Spot_BacktestScenario");
var rsiDivIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
scenario.Indicators = new List<IndicatorBase> { (IndicatorBase)rsiDivIndicator };
scenario.LoopbackPeriod = 15;
scenario.LookbackPeriod = 15;
var config = new TradingBotConfig
{
@@ -570,10 +570,10 @@ public class BacktestExecutorTests : BaseTests, IDisposable
// Assert - Validate specific backtest results
Assert.NotNull(result);
Assert.IsType<LightBacktest>(result);
// Verify TradingType is BacktestSpot
Assert.Equal(TradingType.BacktestSpot, result.Config.TradingType);
// Validate key metrics - Updated with actual backtest results
Assert.Equal(1000.0m, result.InitialBalance);
Assert.Equal(-71.63m, Math.Round(result.FinalPnl, 2));
@@ -586,7 +586,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
Assert.Equal(-0.107, Math.Round((double)(result.SharpeRatio ?? 0), 3));
Assert.True(Math.Abs(result.Score - 0.0) < 0.001,
$"Score {result.Score} should be within 0.001 of expected value 0.0");
// Validate dates
Assert.Equal(new DateTime(2025, 10, 14, 12, 0, 0), result.StartDate);
Assert.Equal(new DateTime(2025, 10, 24, 11, 45, 0), result.EndDate);