Update bot market type

This commit is contained in:
2025-12-11 23:32:06 +07:00
parent 35df25915f
commit a254db6d24
20 changed files with 1986 additions and 44 deletions

View File

@@ -1,4 +1,4 @@
using System.Text.Json; using System.Text.Json;
using Managing.Api.Models.Requests; using Managing.Api.Models.Requests;
using Managing.Api.Models.Responses; using Managing.Api.Models.Responses;
using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Repositories;
@@ -35,6 +35,7 @@ public class BacktestController : BaseController
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IMoneyManagementService _moneyManagementService; private readonly IMoneyManagementService _moneyManagementService;
private readonly IGeneticService _geneticService; private readonly IGeneticService _geneticService;
private readonly IFlagsmithService _flagsmithService;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<BacktestController> _logger; private readonly ILogger<BacktestController> _logger;
@@ -54,6 +55,7 @@ public class BacktestController : BaseController
IAccountService accountService, IAccountService accountService,
IMoneyManagementService moneyManagementService, IMoneyManagementService moneyManagementService,
IGeneticService geneticService, IGeneticService geneticService,
IFlagsmithService flagsmithService,
IUserService userService, IUserService userService,
IServiceScopeFactory serviceScopeFactory, IServiceScopeFactory serviceScopeFactory,
ILogger<BacktestController> logger) : base(userService) ILogger<BacktestController> logger) : base(userService)
@@ -63,6 +65,7 @@ public class BacktestController : BaseController
_accountService = accountService; _accountService = accountService;
_moneyManagementService = moneyManagementService; _moneyManagementService = moneyManagementService;
_geneticService = geneticService; _geneticService = geneticService;
_flagsmithService = flagsmithService;
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_logger = logger; _logger = logger;
} }
@@ -152,7 +155,8 @@ public class BacktestController : BaseController
[FromQuery] string? indicators = null, [FromQuery] string? indicators = null,
[FromQuery] double? durationMinDays = null, [FromQuery] double? durationMinDays = null,
[FromQuery] double? durationMaxDays = null, [FromQuery] double? durationMaxDays = null,
[FromQuery] string? name = null) [FromQuery] string? name = null,
[FromQuery] TradingType? tradingType = null)
{ {
var user = await GetUser(); var user = await GetUser();
@@ -211,7 +215,8 @@ public class BacktestController : BaseController
Tickers = tickerList, Tickers = tickerList,
Indicators = indicatorList, Indicators = indicatorList,
DurationMin = durationMinDays.HasValue ? TimeSpan.FromDays(durationMinDays.Value) : (TimeSpan?)null, DurationMin = durationMinDays.HasValue ? TimeSpan.FromDays(durationMinDays.Value) : (TimeSpan?)null,
DurationMax = durationMaxDays.HasValue ? TimeSpan.FromDays(durationMaxDays.Value) : (TimeSpan?)null DurationMax = durationMaxDays.HasValue ? TimeSpan.FromDays(durationMaxDays.Value) : (TimeSpan?)null,
TradingType = tradingType
}; };
try try
@@ -317,7 +322,8 @@ public class BacktestController : BaseController
ScoreMessage = b.ScoreMessage, ScoreMessage = b.ScoreMessage,
InitialBalance = b.InitialBalance, InitialBalance = b.InitialBalance,
NetPnl = b.NetPnl, NetPnl = b.NetPnl,
PositionCount = b.PositionCount PositionCount = b.PositionCount,
TradingType = b.Config.TradingType
}), }),
TotalCount = totalCount, TotalCount = totalCount,
CurrentPage = page, CurrentPage = page,
@@ -354,7 +360,8 @@ public class BacktestController : BaseController
[FromQuery] string? indicators = null, [FromQuery] string? indicators = null,
[FromQuery] double? durationMinDays = null, [FromQuery] double? durationMinDays = null,
[FromQuery] double? durationMaxDays = null, [FromQuery] double? durationMaxDays = null,
[FromQuery] string? name = null) [FromQuery] string? name = null,
[FromQuery] TradingType? tradingType = null)
{ {
var user = await GetUser(); var user = await GetUser();
@@ -427,7 +434,8 @@ public class BacktestController : BaseController
Tickers = tickerList, Tickers = tickerList,
Indicators = indicatorList, Indicators = indicatorList,
DurationMin = durationMinDays.HasValue ? TimeSpan.FromDays(durationMinDays.Value) : (TimeSpan?)null, DurationMin = durationMinDays.HasValue ? TimeSpan.FromDays(durationMinDays.Value) : (TimeSpan?)null,
DurationMax = durationMaxDays.HasValue ? TimeSpan.FromDays(durationMaxDays.Value) : (TimeSpan?)null DurationMax = durationMaxDays.HasValue ? TimeSpan.FromDays(durationMaxDays.Value) : (TimeSpan?)null,
TradingType = tradingType
}; };
var (backtests, totalCount) = var (backtests, totalCount) =
@@ -459,7 +467,8 @@ public class BacktestController : BaseController
ScoreMessage = b.ScoreMessage, ScoreMessage = b.ScoreMessage,
InitialBalance = b.InitialBalance, InitialBalance = b.InitialBalance,
NetPnl = b.NetPnl, NetPnl = b.NetPnl,
PositionCount = b.PositionCount PositionCount = b.PositionCount,
TradingType = b.Config.TradingType
}), }),
TotalCount = totalCount, TotalCount = totalCount,
CurrentPage = page, CurrentPage = page,
@@ -651,6 +660,20 @@ public class BacktestController : BaseController
{ {
var user = await GetUser(); var user = await GetUser();
// Check if trading type is futures and verify the user has permission via feature flag
if (request.UniversalConfig.TradingType == TradingType.Futures ||
request.UniversalConfig.TradingType == TradingType.BacktestFutures)
{
var isTradingFutureEnabled = await _flagsmithService.IsFeatureEnabledAsync(user.Name, "trading_future");
if (!isTradingFutureEnabled)
{
_logger.LogWarning("User {UserName} attempted to create futures bundle backtest but does not have the trading_future feature flag enabled",
user.Name);
return Forbid("Futures trading is not enabled for your account. Please contact support to enable this feature.");
}
}
if (string.IsNullOrEmpty(request.UniversalConfig.ScenarioName) && request.UniversalConfig.Scenario == null) if (string.IsNullOrEmpty(request.UniversalConfig.ScenarioName) && request.UniversalConfig.Scenario == null)
{ {
return BadRequest("Either scenario name or scenario object is required in universal configuration"); return BadRequest("Either scenario name or scenario object is required in universal configuration");

View File

@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Managing.Domain.Backtests; using Managing.Domain.Backtests;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests; namespace Managing.Api.Models.Requests;
@@ -23,6 +24,7 @@ public class LightBacktestResponse
[Required] public decimal InitialBalance { get; set; } [Required] public decimal InitialBalance { get; set; }
[Required] public decimal NetPnl { get; set; } [Required] public decimal NetPnl { get; set; }
[Required] public int PositionCount { get; set; } [Required] public int PositionCount { get; set; }
[Required] public TradingType TradingType { get; set; }
} }
public static class LightBacktestResponseMapper public static class LightBacktestResponseMapper
@@ -47,7 +49,8 @@ public static class LightBacktestResponseMapper
ScoreMessage = b.ScoreMessage, ScoreMessage = b.ScoreMessage,
InitialBalance = b.InitialBalance, InitialBalance = b.InitialBalance,
NetPnl = b.NetPnl, NetPnl = b.NetPnl,
PositionCount = b.PositionCount PositionCount = b.PositionCount,
TradingType = b.Config.TradingType
}; };
} }
} }

View File

@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Indicators; using Managing.Domain.Indicators;
using Managing.Domain.Trades; using Managing.Domain.Trades;
@@ -82,6 +82,12 @@ namespace Managing.Api.Models.Responses
[Required] [Required]
public Ticker Ticker { get; set; } public Ticker Ticker { get; set; }
/// <summary>
/// The trading type (Futures, Spot, etc.)
/// </summary>
[Required]
public TradingType TradingType { 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)
/// </summary> /// </summary>

View File

@@ -6,27 +6,27 @@ namespace Managing.Application.Abstractions.Services;
public interface IFlagsmithService public interface IFlagsmithService
{ {
/// <summary> /// <summary>
/// Gets flags for a specific user identity /// Gets flags for a specific user. The username is hashed internally for privacy.
/// </summary> /// </summary>
/// <param name="identity">The user identity identifier</param> /// <param name="username">The username to get flags for</param>
/// <returns>Flags object for the identity</returns> /// <returns>Flags object for the user</returns>
Task<IFlagsmithFlags> GetIdentityFlagsAsync(string identity); Task<IFlagsmithFlags> GetIdentityFlagsAsync(string username);
/// <summary> /// <summary>
/// Checks if a feature is enabled for a specific identity /// Checks if a feature is enabled for a specific user. The username is hashed internally for privacy.
/// </summary> /// </summary>
/// <param name="identity">The user identity identifier</param> /// <param name="username">The username to check</param>
/// <param name="featureName">The name of the feature flag</param> /// <param name="featureName">The name of the feature flag</param>
/// <returns>True if the feature is enabled</returns> /// <returns>True if the feature is enabled</returns>
Task<bool> IsFeatureEnabledAsync(string identity, string featureName); Task<bool> IsFeatureEnabledAsync(string username, string featureName);
/// <summary> /// <summary>
/// Gets the feature value for a specific identity /// Gets the feature value for a specific user. The username is hashed internally for privacy.
/// </summary> /// </summary>
/// <param name="identity">The user identity identifier</param> /// <param name="username">The username to get feature value for</param>
/// <param name="featureName">The name of the feature flag</param> /// <param name="featureName">The name of the feature flag</param>
/// <returns>The feature value as string</returns> /// <returns>The feature value as string</returns>
Task<string?> GetFeatureValueAsync(string identity, string featureName); Task<string?> GetFeatureValueAsync(string username, string featureName);
} }
/// <summary> /// <summary>

View File

@@ -1,3 +1,5 @@
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Shared; namespace Managing.Application.Abstractions.Shared;
public class BacktestsFilter public class BacktestsFilter
@@ -12,6 +14,7 @@ public class BacktestsFilter
public IEnumerable<string>? Indicators { get; set; } public IEnumerable<string>? Indicators { get; set; }
public TimeSpan? DurationMin { get; set; } public TimeSpan? DurationMin { get; set; }
public TimeSpan? DurationMax { get; set; } public TimeSpan? DurationMax { get; set; }
public TradingType? TradingType { get; set; }
} }

View File

@@ -1,3 +1,5 @@
using System.Security.Cryptography;
using System.Text;
using Flagsmith; using Flagsmith;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -30,30 +32,32 @@ public class FlagsmithService : IFlagsmithService
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
public async Task<IFlagsmithFlags> GetIdentityFlagsAsync(string identity) public async Task<IFlagsmithFlags> GetIdentityFlagsAsync(string username)
{ {
if (string.IsNullOrWhiteSpace(identity)) if (string.IsNullOrWhiteSpace(username))
{ {
throw new ArgumentException("Identity cannot be null or empty", nameof(identity)); throw new ArgumentException("Username cannot be null or empty", nameof(username));
} }
var hashedIdentity = HashUsername(username);
try try
{ {
var flags = await _flagsmithClient.GetIdentityFlags(identity); var flags = await _flagsmithClient.GetIdentityFlags(hashedIdentity);
return new FlagsmithFlagsWrapper(flags); return new FlagsmithFlagsWrapper(flags);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error getting flags for identity {Identity}", identity); _logger.LogError(ex, "Error getting flags for username {Username} (hashed: {HashedIdentity})", username, hashedIdentity);
throw; throw;
} }
} }
public async Task<bool> IsFeatureEnabledAsync(string identity, string featureName) public async Task<bool> IsFeatureEnabledAsync(string username, string featureName)
{ {
if (string.IsNullOrWhiteSpace(identity)) if (string.IsNullOrWhiteSpace(username))
{ {
throw new ArgumentException("Identity cannot be null or empty", nameof(identity)); throw new ArgumentException("Username cannot be null or empty", nameof(username));
} }
if (string.IsNullOrWhiteSpace(featureName)) if (string.IsNullOrWhiteSpace(featureName))
@@ -63,21 +67,21 @@ public class FlagsmithService : IFlagsmithService
try try
{ {
var flags = await GetIdentityFlagsAsync(identity); var flags = await GetIdentityFlagsAsync(username);
return await flags.IsFeatureEnabled(featureName); return await flags.IsFeatureEnabled(featureName);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error checking feature {FeatureName} for identity {Identity}", featureName, identity); _logger.LogError(ex, "Error checking feature {FeatureName} for username {Username}", featureName, username);
return false; // Default to false on error return false; // Default to false on error
} }
} }
public async Task<string?> GetFeatureValueAsync(string identity, string featureName) public async Task<string?> GetFeatureValueAsync(string username, string featureName)
{ {
if (string.IsNullOrWhiteSpace(identity)) if (string.IsNullOrWhiteSpace(username))
{ {
throw new ArgumentException("Identity cannot be null or empty", nameof(identity)); throw new ArgumentException("Username cannot be null or empty", nameof(username));
} }
if (string.IsNullOrWhiteSpace(featureName)) if (string.IsNullOrWhiteSpace(featureName))
@@ -87,15 +91,32 @@ public class FlagsmithService : IFlagsmithService
try try
{ {
var flags = await GetIdentityFlagsAsync(identity); var flags = await GetIdentityFlagsAsync(username);
return await flags.GetFeatureValue(featureName); return await flags.GetFeatureValue(featureName);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error getting feature value {FeatureName} for identity {Identity}", featureName, identity); _logger.LogError(ex, "Error getting feature value {FeatureName} for username {Username}", featureName, username);
return null; // Default to null on error return null; // Default to null on error
} }
} }
/// <summary>
/// Hashes the username using SHA256 to create a privacy-preserving identity for Flagsmith
/// </summary>
/// <param name="username">The username to hash</param>
/// <returns>SHA256 hash of the username as a hexadecimal string</returns>
private static string HashUsername(string username)
{
if (string.IsNullOrWhiteSpace(username))
{
throw new ArgumentException("Username cannot be null or empty", nameof(username));
}
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(username));
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
} }
/// <summary> /// <summary>

View File

@@ -1,4 +1,4 @@
using Managing.Domain.Users; using Managing.Domain.Users;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Domain.Bots namespace Managing.Domain.Bots
@@ -9,6 +9,7 @@ namespace Managing.Domain.Bots
public Guid Identifier { get; set; } public Guid Identifier { get; set; }
public string Name { get; set; } public string Name { get; set; }
public Ticker Ticker { get; set; } public Ticker Ticker { get; set; }
public TradingType TradingType { get; set; }
public BotStatus Status { get; set; } public BotStatus Status { get; set; }
public DateTime StartupTime { get; set; } public DateTime StartupTime { get; set; }
public DateTime CreateDate { get; set; } public DateTime CreateDate { get; set; }

View File

@@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Managing.Infrastructure.Databases.Migrations
{
/// <inheritdoc />
public partial class AddTradingTypeToBacktestsAndBots : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Ticker",
table: "Bots",
type: "text",
nullable: false,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AddColumn<string>(
name: "TradingType",
table: "Bots",
type: "text",
nullable: false,
defaultValue: "Spot");
migrationBuilder.AddColumn<int>(
name: "TradingType",
table: "Backtests",
type: "integer",
nullable: false,
defaultValue: 1);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TradingType",
table: "Bots");
migrationBuilder.DropColumn(
name: "TradingType",
table: "Backtests");
migrationBuilder.AlterColumn<int>(
name: "Ticker",
table: "Bots",
type: "integer",
nullable: false,
oldClrType: typeof(string),
oldType: "text");
}
}
}

View File

@@ -259,6 +259,9 @@ namespace Managing.Infrastructure.Databases.Migrations
b.Property<int>("Timeframe") b.Property<int>("Timeframe")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<int>("TradingType")
.HasColumnType("integer");
b.Property<DateTime>("UpdatedAt") b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@@ -348,8 +351,9 @@ namespace Managing.Infrastructure.Databases.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<int>("Ticker") b.Property<string>("Ticker")
.HasColumnType("integer"); .IsRequired()
.HasColumnType("text");
b.Property<int>("TradeLosses") b.Property<int>("TradeLosses")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -357,6 +361,10 @@ namespace Managing.Infrastructure.Databases.Migrations
b.Property<int>("TradeWins") b.Property<int>("TradeWins")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("TradingType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt") b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");

View File

@@ -50,6 +50,10 @@ public class BacktestEntity
[Required] [Required]
public int Timeframe { get; set; } public int Timeframe { get; set; }
// Stored trading type as enum numeric value for direct filtering
[Required]
public int TradingType { get; set; }
// Comma-separated indicator types for filtering, e.g., "EMA_CROSS,MACD_CROSS" // Comma-separated indicator types for filtering, e.g., "EMA_CROSS,MACD_CROSS"
[Column(TypeName = "text")] [Column(TypeName = "text")]
public string IndicatorsCsv { get; set; } = string.Empty; public string IndicatorsCsv { get; set; } = string.Empty;

View File

@@ -13,6 +13,8 @@ public class BotEntity
public Ticker Ticker { get; set; } public Ticker Ticker { get; set; }
public TradingType TradingType { get; set; }
public int UserId { get; set; } public int UserId { get; set; }
[ForeignKey("UserId")] public UserEntity User { get; set; } [ForeignKey("UserId")] public UserEntity User { get; set; }

View File

@@ -161,6 +161,7 @@ public class ManagingDbContext : DbContext
entity.Property(e => e.Name).IsRequired().HasMaxLength(255); entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Ticker).HasMaxLength(32); entity.Property(e => e.Ticker).HasMaxLength(32);
entity.Property(e => e.Timeframe).IsRequired(); entity.Property(e => e.Timeframe).IsRequired();
entity.Property(e => e.TradingType).IsRequired();
entity.Property(e => e.IndicatorsCsv).HasColumnType("text"); entity.Property(e => e.IndicatorsCsv).HasColumnType("text");
entity.Property(e => e.IndicatorsCount).IsRequired(); entity.Property(e => e.IndicatorsCount).IsRequired();
entity.Property(e => e.PositionsJson).HasColumnType("jsonb"); entity.Property(e => e.PositionsJson).HasColumnType("jsonb");
@@ -522,6 +523,8 @@ public class ManagingDbContext : DbContext
entity.HasKey(e => e.Identifier); entity.HasKey(e => e.Identifier);
entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255); entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255); entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Ticker).IsRequired().HasConversion<string>();
entity.Property(e => e.TradingType).IsRequired().HasConversion<string>();
entity.Property(e => e.Status).IsRequired().HasConversion<string>(); entity.Property(e => e.Status).IsRequired().HasConversion<string>();
entity.Property(e => e.CreateDate).IsRequired(); entity.Property(e => e.CreateDate).IsRequired();
entity.Property(e => e.StartupTime).IsRequired(); entity.Property(e => e.StartupTime).IsRequired();

View File

@@ -438,6 +438,8 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value); baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value);
if (filter.DurationMax.HasValue) if (filter.DurationMax.HasValue)
baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value); baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value);
if (filter.TradingType.HasValue)
baseQuery = baseQuery.Where(b => b.TradingType == (int)filter.TradingType.Value);
} }
var entities = await baseQuery.ToListAsync().ConfigureAwait(false); var entities = await baseQuery.ToListAsync().ConfigureAwait(false);
@@ -503,6 +505,8 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value); baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value);
if (filter.DurationMax.HasValue) if (filter.DurationMax.HasValue)
baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value); baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value);
if (filter.TradingType.HasValue)
baseQuery = baseQuery.Where(b => b.TradingType == (int)filter.TradingType.Value);
} }
var afterQueryMs = stopwatch.ElapsedMilliseconds; var afterQueryMs = stopwatch.ElapsedMilliseconds;
@@ -642,6 +646,8 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value); baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value);
if (filter.DurationMax.HasValue) if (filter.DurationMax.HasValue)
baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value); baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value);
if (filter.TradingType.HasValue)
baseQuery = baseQuery.Where(b => b.TradingType == (int)filter.TradingType.Value);
} }
var afterQueryMs = stopwatch.ElapsedMilliseconds; var afterQueryMs = stopwatch.ElapsedMilliseconds;

View File

@@ -350,6 +350,7 @@ public static class PostgreSqlMappers
Name = backtest.Config?.Name ?? string.Empty, Name = backtest.Config?.Name ?? string.Empty,
Ticker = backtest.Config?.Ticker.ToString() ?? string.Empty, Ticker = backtest.Config?.Ticker.ToString() ?? string.Empty,
Timeframe = (int)backtest.Config.Timeframe, Timeframe = (int)backtest.Config.Timeframe,
TradingType = (int)backtest.Config.TradingType,
IndicatorsCsv = string.Join(',', backtest.Config.Scenario.Indicators.Select(i => i.Type.ToString())), IndicatorsCsv = string.Join(',', backtest.Config.Scenario.Indicators.Select(i => i.Type.ToString())),
IndicatorsCount = backtest.Config.Scenario.Indicators.Count, IndicatorsCount = backtest.Config.Scenario.Indicators.Count,
PositionsJson = JsonConvert.SerializeObject(backtest.Positions.Values.ToList(), jsonSettings), PositionsJson = JsonConvert.SerializeObject(backtest.Positions.Values.ToList(), jsonSettings),
@@ -750,6 +751,7 @@ public static class PostgreSqlMappers
CreateDate = entity.CreateDate, CreateDate = entity.CreateDate,
Name = entity.Name, Name = entity.Name,
Ticker = entity.Ticker, Ticker = entity.Ticker,
TradingType = entity.TradingType,
StartupTime = entity.StartupTime, StartupTime = entity.StartupTime,
LastStartTime = entity.LastStartTime, LastStartTime = entity.LastStartTime,
LastStopTime = entity.LastStopTime, LastStopTime = entity.LastStopTime,
@@ -782,6 +784,7 @@ public static class PostgreSqlMappers
CreateDate = bot.CreateDate, CreateDate = bot.CreateDate,
Name = bot.Name, Name = bot.Name,
Ticker = bot.Ticker, Ticker = bot.Ticker,
TradingType = bot.TradingType,
StartupTime = bot.StartupTime, StartupTime = bot.StartupTime,
LastStartTime = bot.LastStartTime, LastStartTime = bot.LastStartTime,
LastStopTime = bot.LastStopTime, LastStopTime = bot.LastStopTime,

View File

@@ -987,6 +987,7 @@ export interface TradingBotResponse {
startupTime: Date; startupTime: Date;
name: string; name: string;
ticker: Ticker; ticker: Ticker;
tradingType: TradingType;
masterAgentName?: string | null; masterAgentName?: string | null;
} }

View File

@@ -5,7 +5,7 @@ import {useExpanded, useFilters, usePagination, useSortBy, useTable,} from 'reac
import useApiUrlStore from '../../../app/store/apiStore' import useApiUrlStore from '../../../app/store/apiStore'
import useBacktestStore from '../../../app/store/backtestStore' import useBacktestStore from '../../../app/store/backtestStore'
import type {Backtest, LightBacktestResponse} from '../../../generated/ManagingApi' import type {Backtest, LightBacktestResponse} from '../../../generated/ManagingApi'
import {BacktestClient, BacktestSortableColumn, IndicatorType} from '../../../generated/ManagingApi' import {BacktestClient, BacktestSortableColumn, IndicatorType, TradingType} from '../../../generated/ManagingApi'
import {ConfigDisplayModal, IndicatorsDisplay, SelectColumnFilter} from '../../mollecules' import {ConfigDisplayModal, IndicatorsDisplay, SelectColumnFilter} from '../../mollecules'
import {UnifiedTradingModal} from '../index' import {UnifiedTradingModal} from '../index'
import Toast from '../../mollecules/Toast/Toast' import Toast from '../../mollecules/Toast/Toast'
@@ -148,6 +148,7 @@ interface BacktestTableProps {
indicators?: string[] | null indicators?: string[] | null
durationMinDays?: number | null durationMinDays?: number | null
durationMaxDays?: number | null durationMaxDays?: number | null
tradingType?: TradingType | null
}) => void }) => void
filters?: { filters?: {
nameContains?: string | null nameContains?: string | null
@@ -160,6 +161,7 @@ interface BacktestTableProps {
indicators?: string[] | null indicators?: string[] | null
durationMinDays?: number | null durationMinDays?: number | null
durationMaxDays?: number | null durationMaxDays?: number | null
tradingType?: TradingType | null
} }
openFiltersTrigger?: number // When this changes, open the filter sidebar openFiltersTrigger?: number // When this changes, open the filter sidebar
} }
@@ -196,6 +198,7 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
const [selectedIndicators, setSelectedIndicators] = useState<string[]>([]) const [selectedIndicators, setSelectedIndicators] = useState<string[]>([])
const [durationMinDays, setDurationMinDays] = useState<number | null>(null) const [durationMinDays, setDurationMinDays] = useState<number | null>(null)
const [durationMaxDays, setDurationMaxDays] = useState<number | null>(null) const [durationMaxDays, setDurationMaxDays] = useState<number | null>(null)
const [selectedTradingType, setSelectedTradingType] = useState<TradingType | null>(null)
// Delete confirmation state // Delete confirmation state
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
@@ -227,6 +230,7 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
setSelectedIndicators([]) setSelectedIndicators([])
setDurationMinDays(null) setDurationMinDays(null)
setDurationMaxDays(null) setDurationMaxDays(null)
setSelectedTradingType(null)
} }
// Refresh data function // Refresh data function
@@ -249,6 +253,7 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
indicators: selectedIndicators.length ? selectedIndicators : null, indicators: selectedIndicators.length ? selectedIndicators : null,
durationMinDays, durationMinDays,
durationMaxDays, durationMaxDays,
tradingType: selectedTradingType,
}) })
setIsFilterOpen(false) setIsFilterOpen(false)
} }
@@ -270,7 +275,8 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
filters.indicators?.join(',') || undefined, filters.indicators?.join(',') || undefined,
filters.durationMinDays || undefined, filters.durationMinDays || undefined,
filters.durationMaxDays || undefined, filters.durationMaxDays || undefined,
filters.nameContains || undefined filters.nameContains || undefined,
filters.tradingType || undefined
) )
// Parse the response to get the deleted count // Parse the response to get the deleted count
@@ -305,7 +311,8 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
filters.tickers?.length || filters.tickers?.length ||
filters.indicators?.length || filters.indicators?.length ||
filters.durationMinDays !== null || filters.durationMinDays !== null ||
filters.durationMaxDays !== null filters.durationMaxDays !== null ||
filters.tradingType !== null
) )
} }
@@ -335,6 +342,7 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
setSelectedIndicators(filters.indicators ? [...filters.indicators] : []) setSelectedIndicators(filters.indicators ? [...filters.indicators] : [])
setDurationMinDays(filters.durationMinDays ?? null) setDurationMinDays(filters.durationMinDays ?? null)
setDurationMaxDays(filters.durationMaxDays ?? null) setDurationMaxDays(filters.durationMaxDays ?? null)
setSelectedTradingType(filters.tradingType ?? null)
}, [filters]) }, [filters])
// Handle external trigger to open filters // Handle external trigger to open filters
@@ -507,6 +515,16 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
accessor: 'config.timeframe', accessor: 'config.timeframe',
disableSortBy: true, disableSortBy: true,
}, },
{
Filter: SelectColumnFilter,
Header: 'Trading Type',
accessor: 'tradingType',
Cell: ({cell}: any) => {
const tradingType = cell.value as TradingType;
return <span className="badge badge-outline">{tradingType}</span>;
},
disableSortBy: true,
},
{ {
Header: 'Indicators', Header: 'Indicators',
accessor: 'config.scenario.indicators', accessor: 'config.scenario.indicators',
@@ -798,6 +816,25 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
</div> </div>
</div> </div>
{/* Trading Type */}
<div className="mb-6">
<div className="mb-2 font-medium">Trading Type</div>
<div className="flex flex-wrap gap-2">
{Object.values(TradingType).map((type) => (
<button
key={type}
className={`btn btn-xs ${selectedTradingType === type ? 'btn-primary' : 'btn-outline'}`}
onClick={() => setSelectedTradingType(selectedTradingType === type ? null : type)}
>
{type}
</button>
))}
{selectedTradingType && (
<button className="btn btn-xs btn-ghost" onClick={() => setSelectedTradingType(null)}>Clear</button>
)}
</div>
</div>
<div className="flex gap-2 justify-end"> <div className="flex gap-2 justify-end">
<button className="btn btn-ghost btn-sm" onClick={() => setIsFilterOpen(false)}>Cancel</button> <button className="btn btn-ghost btn-sm" onClick={() => setIsFilterOpen(false)}>Cancel</button>
<button className="btn btn-primary btn-sm" onClick={applyFilters}>Apply</button> <button className="btn btn-primary btn-sm" onClick={applyFilters}>Apply</button>

View File

@@ -797,7 +797,7 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<FileResponse>(null as any); return Promise.resolve<FileResponse>(null as any);
} }
backtest_DeleteBacktestsByFilters(scoreMin: number | null | undefined, scoreMax: number | null | undefined, winrateMin: number | null | undefined, winrateMax: number | null | undefined, maxDrawdownMax: number | null | undefined, tickers: string | null | undefined, indicators: string | null | undefined, durationMinDays: number | null | undefined, durationMaxDays: number | null | undefined, name: string | null | undefined): Promise<FileResponse> { backtest_DeleteBacktestsByFilters(scoreMin: number | null | undefined, scoreMax: number | null | undefined, winrateMin: number | null | undefined, winrateMax: number | null | undefined, maxDrawdownMax: number | null | undefined, tickers: string | null | undefined, indicators: string | null | undefined, durationMinDays: number | null | undefined, durationMaxDays: number | null | undefined, name: string | null | undefined, tradingType: TradingType | null | undefined): Promise<FileResponse> {
let url_ = this.baseUrl + "/Backtest/ByFilters?"; let url_ = this.baseUrl + "/Backtest/ByFilters?";
if (scoreMin !== undefined && scoreMin !== null) if (scoreMin !== undefined && scoreMin !== null)
url_ += "scoreMin=" + encodeURIComponent("" + scoreMin) + "&"; url_ += "scoreMin=" + encodeURIComponent("" + scoreMin) + "&";
@@ -819,6 +819,8 @@ export class BacktestClient extends AuthorizedApiBase {
url_ += "durationMaxDays=" + encodeURIComponent("" + durationMaxDays) + "&"; url_ += "durationMaxDays=" + encodeURIComponent("" + durationMaxDays) + "&";
if (name !== undefined && name !== null) if (name !== undefined && name !== null)
url_ += "name=" + encodeURIComponent("" + name) + "&"; url_ += "name=" + encodeURIComponent("" + name) + "&";
if (tradingType !== undefined && tradingType !== null)
url_ += "tradingType=" + encodeURIComponent("" + tradingType) + "&";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = { let options_: RequestInit = {
@@ -945,7 +947,7 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<PaginatedBacktestsResponse>(null as any); return Promise.resolve<PaginatedBacktestsResponse>(null as any);
} }
backtest_GetBacktestsPaginated(page: number | undefined, pageSize: number | undefined, sortBy: BacktestSortableColumn | undefined, sortOrder: string | null | undefined, scoreMin: number | null | undefined, scoreMax: number | null | undefined, winrateMin: number | null | undefined, winrateMax: number | null | undefined, maxDrawdownMax: number | null | undefined, tickers: string | null | undefined, indicators: string | null | undefined, durationMinDays: number | null | undefined, durationMaxDays: number | null | undefined, name: string | null | undefined): Promise<PaginatedBacktestsResponse> { backtest_GetBacktestsPaginated(page: number | undefined, pageSize: number | undefined, sortBy: BacktestSortableColumn | undefined, sortOrder: string | null | undefined, scoreMin: number | null | undefined, scoreMax: number | null | undefined, winrateMin: number | null | undefined, winrateMax: number | null | undefined, maxDrawdownMax: number | null | undefined, tickers: string | null | undefined, indicators: string | null | undefined, durationMinDays: number | null | undefined, durationMaxDays: number | null | undefined, name: string | null | undefined, tradingType: TradingType | null | undefined): Promise<PaginatedBacktestsResponse> {
let url_ = this.baseUrl + "/Backtest/Paginated?"; let url_ = this.baseUrl + "/Backtest/Paginated?";
if (page === null) if (page === null)
throw new Error("The parameter 'page' cannot be null."); throw new Error("The parameter 'page' cannot be null.");
@@ -981,6 +983,8 @@ export class BacktestClient extends AuthorizedApiBase {
url_ += "durationMaxDays=" + encodeURIComponent("" + durationMaxDays) + "&"; url_ += "durationMaxDays=" + encodeURIComponent("" + durationMaxDays) + "&";
if (name !== undefined && name !== null) if (name !== undefined && name !== null)
url_ += "name=" + encodeURIComponent("" + name) + "&"; url_ += "name=" + encodeURIComponent("" + name) + "&";
if (tradingType !== undefined && tradingType !== null)
url_ += "tradingType=" + encodeURIComponent("" + tradingType) + "&";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = { let options_: RequestInit = {
@@ -5482,6 +5486,7 @@ export interface TradingBotResponse {
startupTime: Date; startupTime: Date;
name: string; name: string;
ticker: Ticker; ticker: Ticker;
tradingType: TradingType;
masterAgentName?: string | null; masterAgentName?: string | null;
} }

View File

@@ -987,6 +987,7 @@ export interface TradingBotResponse {
startupTime: Date; startupTime: Date;
name: string; name: string;
ticker: Ticker; ticker: Ticker;
tradingType: TradingType;
masterAgentName?: string | null; masterAgentName?: string | null;
} }

View File

@@ -7,7 +7,7 @@ import {Loader, Slider} from '../../components/atoms'
import {BottomMenuBar, Modal, Toast} from '../../components/mollecules' import {BottomMenuBar, Modal, Toast} from '../../components/mollecules'
import {BacktestTable, UnifiedTradingModal} from '../../components/organism' import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
import type {LightBacktestResponse} from '../../generated/ManagingApi' import type {LightBacktestResponse} from '../../generated/ManagingApi'
import {BacktestClient, BacktestSortableColumn} from '../../generated/ManagingApi' import {BacktestClient, BacktestSortableColumn, TradingType} from '../../generated/ManagingApi'
const PAGE_SIZE = 50 const PAGE_SIZE = 50
@@ -38,6 +38,7 @@ const BacktestScanner: React.FC = () => {
indicators?: string[] | null indicators?: string[] | null
durationMinDays?: number | null durationMinDays?: number | null
durationMaxDays?: number | null durationMaxDays?: number | null
tradingType?: TradingType | null
}>({}) }>({})
const { apiUrl } = useApiUrlStore() const { apiUrl } = useApiUrlStore()
@@ -69,6 +70,7 @@ const BacktestScanner: React.FC = () => {
filters.durationMinDays ?? null, filters.durationMinDays ?? null,
filters.durationMaxDays ?? null, filters.durationMaxDays ?? null,
filters.nameContains ?? null, filters.nameContains ?? null,
filters.tradingType ?? null,
) )
return { return {
backtests: (response.backtests as LightBacktestResponse[]) || [], backtests: (response.backtests as LightBacktestResponse[]) || [],
@@ -217,6 +219,7 @@ const BacktestScanner: React.FC = () => {
indicators?: string[] | null indicators?: string[] | null
durationMinDays?: number | null durationMinDays?: number | null
durationMaxDays?: number | null durationMaxDays?: number | null
tradingType?: TradingType | null
}) => { }) => {
setFilters(newFilters) setFilters(newFilters)
setCurrentPage(1) setCurrentPage(1)
@@ -246,7 +249,8 @@ const BacktestScanner: React.FC = () => {
(filters.tickers && filters.tickers.length) || (filters.tickers && filters.tickers.length) ||
(filters.indicators && filters.indicators.length) || (filters.indicators && filters.indicators.length) ||
(filters.durationMinDays !== undefined && filters.durationMinDays !== null) || (filters.durationMinDays !== undefined && filters.durationMinDays !== null) ||
(filters.durationMaxDays !== undefined && filters.durationMaxDays !== null) (filters.durationMaxDays !== undefined && filters.durationMaxDays !== null) ||
(filters.tradingType !== undefined && filters.tradingType !== null)
) ? ( ) ? (
<div className="flex flex-wrap gap-2 items-center"> <div className="flex flex-wrap gap-2 items-center">
<span className="text-sm opacity-70 mr-1">Active filters:</span> <span className="text-sm opacity-70 mr-1">Active filters:</span>
@@ -274,6 +278,9 @@ const BacktestScanner: React.FC = () => {
{(filters.durationMinDays !== undefined && filters.durationMinDays !== null) || (filters.durationMaxDays !== undefined && filters.durationMaxDays !== null) ? ( {(filters.durationMinDays !== undefined && filters.durationMinDays !== null) || (filters.durationMaxDays !== undefined && filters.durationMaxDays !== null) ? (
<div className="badge badge-outline">Duration: {filters.durationMinDays ?? 0}{filters.durationMaxDays ?? '∞'} days</div> <div className="badge badge-outline">Duration: {filters.durationMinDays ?? 0}{filters.durationMaxDays ?? '∞'} days</div>
) : null} ) : null}
{filters.tradingType !== undefined && filters.tradingType !== null && (
<div className="badge badge-outline">Trading Type: {filters.tradingType}</div>
)}
</div> </div>
) : null} ) : null}
</div> </div>