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) if (request.Config.Scenario != null)
{ {
// Convert ScenarioRequest to Scenario domain object // 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 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.Api.Models.Responses;
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services; 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="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="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="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> /// <returns>A paginated response containing trading bots</returns>
[HttpGet] [HttpGet]
[Route("Paginated")] [Route("Paginated")]
@@ -442,7 +442,7 @@ public class BotController : BaseController
string? ticker = null, string? ticker = null,
string? agentName = null, string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate, BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc") SortDirection sortDirection = SortDirection.Desc)
{ {
try try
{ {
@@ -770,7 +770,7 @@ public class BotController : BaseController
if (request.Config.Scenario != null) if (request.Config.Scenario != null)
{ {
// Convert ScenarioRequest to Scenario domain object // 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 User = user
}; };
@@ -931,7 +931,7 @@ public class BotController : BaseController
if (request.Config.Scenario != null) if (request.Config.Scenario != null)
{ {
// Convert ScenarioRequest to Scenario domain object // 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 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.Api.Models.Responses;
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains; 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) // Use caching for position data in UI context (not critical trading operations)
var cacheKey = $"positions_{strategy.Identifier}"; var cacheKey = $"positions_{strategy.Identifier}";
var cachedPositions = _cacheService.GetValue<List<Position>>(cacheKey); var cachedPositions = _cacheService.GetValue<List<Position>>(cacheKey);
List<Position> positions; List<Position> positions;
if (cachedPositions != null) if (cachedPositions != null)
{ {
@@ -571,7 +571,7 @@ public class DataController : ControllerBase
{ {
// Fetch positions associated with this bot using the provided trading service // Fetch positions associated with this bot using the provided trading service
positions = (await tradingService.GetPositionsByInitiatorIdentifierAsync(strategy.Identifier)).ToList(); positions = (await tradingService.GetPositionsByInitiatorIdentifierAsync(strategy.Identifier)).ToList();
// Cache positions for 2 minutes for UI display purposes // Cache positions for 2 minutes for UI display purposes
_cacheService.SaveValue(cacheKey, positions, TimeSpan.FromMinutes(2)); _cacheService.SaveValue(cacheKey, positions, TimeSpan.FromMinutes(2));
} }
@@ -619,9 +619,9 @@ public class DataController : ControllerBase
NetPnL = strategy.NetPnL, NetPnL = strategy.NetPnL,
ROIPercentage = strategy.Roi, ROIPercentage = strategy.Roi,
Runtime = strategy.Status == BotStatus.Running ? strategy.LastStartTime : null, Runtime = strategy.Status == BotStatus.Running ? strategy.LastStartTime : null,
LastStartTime = strategy.LastStartTime, LastStartTime = strategy.LastStartTime,
LastStopTime = strategy.LastStopTime, LastStopTime = strategy.LastStopTime,
AccumulatedRunTimeSeconds = strategy.AccumulatedRunTimeSeconds, AccumulatedRunTimeSeconds = strategy.AccumulatedRunTimeSeconds,
TotalRuntimeSeconds = strategy.GetTotalRuntimeSeconds(), TotalRuntimeSeconds = strategy.GetTotalRuntimeSeconds(),
WinRate = winRate, WinRate = winRate,
TotalVolumeTraded = totalVolume, TotalVolumeTraded = totalVolume,
@@ -715,7 +715,9 @@ public class DataController : ControllerBase
var showOnlyProfitable = _configuration.GetValue<bool>("showOnlyProfitable", false); var showOnlyProfitable = _configuration.GetValue<bool>("showOnlyProfitable", false);
// Get paginated results from database // 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 result = await _mediator.Send(command);
var agentSummaries = result.Results; var agentSummaries = result.Results;
var totalCount = result.TotalCount; var totalCount = result.TotalCount;
@@ -819,7 +821,7 @@ public class DataController : ControllerBase
/// <returns>A domain Scenario object.</returns> /// <returns>A domain Scenario object.</returns>
private Scenario MapScenarioRequestToScenario(ScenarioRequest scenarioRequest) 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) 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="ticker">Filter by ticker (partial match, case-insensitive)</param>
/// <param name="agentName">Filter by agent name (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="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> /// <returns>A paginated list of strategies excluding Saved status bots</returns>
[HttpGet("GetStrategiesPaginated")] [HttpGet("GetStrategiesPaginated")]
public async Task<ActionResult<PaginatedResponse<TradingBotResponse>>> GetStrategiesPaginated( public async Task<ActionResult<PaginatedResponse<TradingBotResponse>>> GetStrategiesPaginated(
@@ -917,7 +919,7 @@ public class DataController : ControllerBase
string? ticker = null, string? ticker = null,
string? agentName = null, string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate, BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc") SortDirection sortDirection = SortDirection.Desc)
{ {
// Validate pagination parameters // Validate pagination parameters
if (pageNumber < 1) if (pageNumber < 1)
@@ -930,11 +932,6 @@ public class DataController : ControllerBase
return BadRequest("Page size must be between 1 and 100"); 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 try
{ {

View File

@@ -191,7 +191,7 @@ public class ScenarioController : BaseController
return new ScenarioViewModel return new ScenarioViewModel
{ {
Name = scenario.Name, Name = scenario.Name,
LoopbackPeriod = scenario.LoopbackPeriod, LoopbackPeriod = scenario.LookbackPeriod,
UserName = scenario.User?.Name, UserName = scenario.User?.Name,
Indicators = scenario.Indicators?.Select(MapToIndicatorViewModel).ToList() ?? new List<IndicatorViewModel>() 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; public BotSortableColumn SortBy { get; set; } = BotSortableColumn.CreateDate;
/// <summary> /// <summary>
/// Sort direction. Default is "Desc" (descending). /// Sort direction. Default is Desc (descending).
/// </summary> /// </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; using Managing.Common;
namespace Managing.Api.Models.Responses namespace Managing.Api.Models.Responses
@@ -10,41 +11,49 @@ namespace Managing.Api.Models.Responses
/// <summary> /// <summary>
/// Name of the deployed strategy /// Name of the deployed strategy
/// </summary> /// </summary>
[Required]
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// Current state of the strategy (RUNNING, STOPPED, UNUSED) /// Current state of the strategy (RUNNING, STOPPED, UNUSED)
/// </summary> /// </summary>
[Required]
public Enums.BotStatus State { get; set; } public Enums.BotStatus State { get; set; }
/// <summary> /// <summary>
/// Total profit or loss generated by the strategy in USD (gross, before fees) /// Total profit or loss generated by the strategy in USD (gross, before fees)
/// </summary> /// </summary>
[Required]
public decimal PnL { get; set; } public decimal PnL { get; set; }
/// <summary> /// <summary>
/// Net profit or loss generated by the strategy in USD (after fees) /// Net profit or loss generated by the strategy in USD (after fees)
/// </summary> /// </summary>
[Required]
public decimal NetPnL { get; set; } public decimal NetPnL { get; set; }
/// <summary> /// <summary>
/// Return on investment percentage /// Return on investment percentage
/// </summary> /// </summary>
[Required]
public decimal ROIPercentage { get; set; } public decimal ROIPercentage { get; set; }
/// <summary> /// <summary>
/// Date and time when the strategy was started (only present when running, for live ticker) /// Date and time when the strategy was started (only present when running, for live ticker)
/// </summary> /// </summary>
[Required]
public DateTime? Runtime { get; set; } public DateTime? Runtime { get; set; }
/// <summary> /// <summary>
/// Total accumulated runtime in seconds (including current session if running) /// Total accumulated runtime in seconds (including current session if running)
/// </summary> /// </summary>
[Required]
public long TotalRuntimeSeconds { get; set; } public long TotalRuntimeSeconds { get; set; }
/// <summary> /// <summary>
/// Time when the current or last session started /// Time when the current or last session started
/// </summary> /// </summary>
[Required]
public DateTime? LastStartTime { get; set; } public DateTime? LastStartTime { get; set; }
/// <summary> /// <summary>
@@ -55,42 +64,49 @@ namespace Managing.Api.Models.Responses
/// <summary> /// <summary>
/// Total accumulated runtime across all past sessions (seconds) /// Total accumulated runtime across all past sessions (seconds)
/// </summary> /// </summary>
[Required]
public long AccumulatedRunTimeSeconds { get; set; } public long AccumulatedRunTimeSeconds { get; set; }
/// <summary> /// <summary>
/// Average percentage of successful trades /// Average percentage of successful trades
/// </summary> /// </summary>
[Required]
public int WinRate { get; set; } public int WinRate { get; set; }
/// <summary> /// <summary>
/// Total trading volume for all trades /// Total trading volume for all trades
/// </summary> /// </summary>
[Required]
public decimal TotalVolumeTraded { get; set; } public decimal TotalVolumeTraded { get; set; }
/// <summary> /// <summary>
/// Trading volume in the last 24 hours /// Trading volume in the last 24 hours
/// </summary> /// </summary>
[Required]
public decimal VolumeLast24H { get; set; } public decimal VolumeLast24H { get; set; }
/// <summary> /// <summary>
/// Number of winning trades /// Number of winning trades
/// </summary> /// </summary>
[Required]
public int Wins { get; set; } public int Wins { get; set; }
/// <summary> /// <summary>
/// Number of losing trades /// Number of losing trades
/// </summary> /// </summary>
[Required]
public int Losses { get; set; } public int Losses { get; set; }
/// <summary> /// <summary>
/// Dictionary of all positions executed by this strategy, keyed by position identifier /// Dictionary of all positions executed by this strategy, keyed by position identifier
/// </summary> /// </summary>
[Required]
public List<PositionViewModel> Positions { get; set; } = new List<PositionViewModel>(); 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 Dictionary<DateTime, decimal> WalletBalances { get; set; } = new Dictionary<DateTime, decimal>();
public Enums.Ticker Ticker { get; set; } [Required] public Enums.Ticker Ticker { get; set; }
/// <summary> /// <summary>
/// The agent name of the master bot's owner (for copy trading bots) /// 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? ticker = null,
string? agentName = null, string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate, BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc", SortDirection sortDirection = SortDirection.Desc,
bool showOnlyProfitable = false); bool showOnlyProfitable = false);
/// <summary> /// <summary>

View File

@@ -158,7 +158,7 @@ public class BacktestTests : BaseTests
var scenario = new Scenario("ETH_BacktestScenario"); var scenario = new Scenario("ETH_BacktestScenario");
var rsiDivIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14); var rsiDivIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
scenario.Indicators = new List<IndicatorBase> { (IndicatorBase)rsiDivIndicator }; scenario.Indicators = new List<IndicatorBase> { (IndicatorBase)rsiDivIndicator };
scenario.LoopbackPeriod = 15; scenario.LookbackPeriod = 15;
var config = new TradingBotConfig 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="ticker">Filter by ticker (partial match, case-insensitive)</param>
/// <param name="agentName">Filter by agent name (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="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> /// <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> /// <returns>Tuple containing the bots for the current page and total count</returns>
Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync( Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
@@ -58,7 +58,7 @@ public interface IBotService
string? ticker = null, string? ticker = null,
string? agentName = null, string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate, BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc", SortDirection sortDirection = SortDirection.Desc,
bool showOnlyProfitable = false); bool showOnlyProfitable = false);
/// <summary> /// <summary>

View File

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

View File

@@ -151,7 +151,7 @@ public class JobService
if (backtestRequest.Config.Scenario != null) if (backtestRequest.Config.Scenario != null)
{ {
var sReq = backtestRequest.Config.Scenario; 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) 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"); throw new ArgumentNullException(nameof(Config.Scenario), "Config.Scenario cannot be null");
// Use TradingBox.GetSignal for backtest with pre-calculated indicators // 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); preCalculatedIndicatorValues);
if (backtestSignal == null) return; 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"); throw new ArgumentNullException(nameof(Config.Scenario), "Config.Scenario cannot be null");
// Use TradingBox.GetSignal for backtest with pre-calculated indicators // 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); preCalculatedIndicatorValues);
if (backtestSignal == null) return; if (backtestSignal == null) return;

View File

@@ -487,7 +487,7 @@ namespace Managing.Application.ManageBot
string? ticker = null, string? ticker = null,
string? agentName = null, string? agentName = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate, BotSortableColumn sortBy = BotSortableColumn.CreateDate,
string sortDirection = "Desc", SortDirection sortDirection = SortDirection.Desc,
bool showOnlyProfitable = false) bool showOnlyProfitable = false)
{ {
return await ServiceScopeHelpers.WithScopedService<IBotRepository, (IEnumerable<Bot> Bots, int TotalCount)>( 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) // Get candles as ordered List (already ordered by date from CandleStoreGrain)
var candlesList = await GetCandlesAsync(tradingExchanges, config); var candlesList = await GetCandlesAsync(tradingExchanges, config);
if (candlesList.Count == 0) if (candlesList.Count == 0)
{ {
_logger.LogWarning($"No candles available for {config.Ticker} for {config.Name}"); _logger.LogWarning($"No candles available for {config.Ticker} for {config.Name}");
@@ -88,7 +88,7 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
candlesList, candlesList,
config.Scenario, config.Scenario,
previousSignals, previousSignals,
config.Scenario?.LoopbackPeriod ?? 1); config.Scenario?.LookbackPeriod ?? 1);
if (signal != null && signal.Date > candle.Date) if (signal != null && signal.Date > candle.Date)
{ {

View File

@@ -20,7 +20,7 @@ namespace Managing.Application.Scenarios
_tradingService = tradingService; _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); var scenario = new Scenario(name, loopbackPeriod);
@@ -77,7 +77,7 @@ namespace Managing.Application.Scenarios
scenario.AddIndicator(await _tradingService.GetIndicatorByNameAsync(strategy)); scenario.AddIndicator(await _tradingService.GetIndicatorByNameAsync(strategy));
} }
scenario.LoopbackPeriod = loopbackPeriod ?? 1; scenario.LookbackPeriod = loopbackPeriod ?? 1;
await _tradingService.UpdateScenarioAsync(scenario); await _tradingService.UpdateScenarioAsync(scenario);
return true; return true;
} }
@@ -262,7 +262,7 @@ namespace Managing.Application.Scenarios
} }
scenario.Indicators.Clear(); scenario.Indicators.Clear();
scenario.LoopbackPeriod = loopbackPeriod ?? 1; scenario.LookbackPeriod = loopbackPeriod ?? 1;
foreach (var strategyName in strategies) foreach (var strategyName in strategies)
{ {

View File

@@ -225,7 +225,7 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
if (runBacktestRequest.Config.Scenario != null) if (runBacktestRequest.Config.Scenario != null)
{ {
var sReq = runBacktestRequest.Config.Scenario; 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) 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 public static class Enums
{ {
@@ -101,6 +101,15 @@ public static class Enums
AgentName AgentName
} }
/// <summary>
/// Sort direction for pagination endpoints
/// </summary>
public enum SortDirection
{
Asc,
Desc
}
public enum SignalStatus public enum SignalStatus
{ {
WaitingForPosition, WaitingForPosition,

View File

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

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Orleans; using Orleans;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -18,11 +19,11 @@ public class LightIndicator
SignalType = ScenarioHelpers.GetSignalType(type); 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; } [Id(3)] public int MinimumHistory { get; set; }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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