## Summary I've successfully implemented all the requested features to add BacktestId support to the strategies/bot system: ### 1. **Added BacktestId Column to BotEntity** ✅ - **File**: `src/Managing.Infrastructure.Database/PostgreSql/Entities/BotEntity.cs:47` - Added nullable `int? BacktestId` property with documentation - This allows bots to reference the backtest they were created from ### 2. **Updated Bot Domain Model** ✅ - **File**: `src/Managing.Domain/Bots/Bot.cs:37` - Added `BacktestId` property to the domain model - Maintains consistency between entity and domain layers ### 3. **Updated TradingBotConfig and TradingBotConfigRequest** ✅ - **File**: `src/Managing.Domain/Bots/TradingBotConfig.cs:131` - Added `[Id(24)] public int? BacktestId` with Orleans serialization attribute - **File**: `src/Managing.Domain/Bots/TradingBotConfigRequest.cs:119` - Added `BacktestId` property to the request model - These changes ensure BacktestId flows through the entire bot creation pipeline ### 4. **Updated Data Mappers** ✅ - **File**: `src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs` - Updated `Map(Bot bot)` at line 833 to include BacktestId - Updated `Map(BotEntity entity)` at line 799 to include BacktestId - Ensures proper mapping between entity and domain models ### 5. **Updated LiveTradingBotGrain** ✅ - **File**: `src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs` - Added BacktestId to bot creation at line 1078 (for saved bots) - Added BacktestId to bot creation at line 1145 (for running bots) - The grain now properly persists BacktestId when saving bot statistics ### 6. **Created New Endpoint for Backtest Stats** ✅ - **File**: `src/Managing.Api/Controllers/BacktestController.cs:114` - **New Endpoint**: `GET /Backtest/{id}/stats` - Returns only statistical information without positions, signals, or candles: - Basic info: id, name, ticker, timeframe, tradingType, startDate, endDate - Performance: initialBalance, finalPnl, netPnl, growthPercentage, hodlPercentage, winRate - Risk metrics: sharpeRatio, maxDrawdown, maxDrawdownRecoveryTime - Other: fees, score, scoreMessage, positionCount ### 7. **Created Database Migration** ✅ - **Generated Migration**: `AddBacktestIdToBots` - The migration adds a nullable integer column `BacktestId` to the `Bots` table - Ready to be applied with `dotnet ef database update` ### 8. **Regenerated Frontend API Client** ✅ - Ran `dotnet build` in `src/Managing.Nswag` - The `ManagingApi.ts` file has been regenerated with: - `backtestId` field in bot-related DTOs - New `/Backtest/{id}/stats` endpoint ## How It Works ### Starting a Bot from a Backtest: 1. Frontend sends `StartBotRequest` with `TradingBotConfigRequest` containing `backtestId` 2. `BotController` validates and prepares the request 3. `StartBotCommandHandler` creates the bot configuration with BacktestId 4. `LiveTradingBotGrain.CreateAsync()` receives the config and saves it to state 5. When the bot is saved via `SaveBotAsync()`, BacktestId is persisted to the database 6. The Bot entity now has a reference to its originating backtest ### Retrieving Backtest Stats: 1. Frontend calls `GET /Backtest/{id}/stats` with the backtest ID 2. Backend retrieves the full backtest from the database 3. Returns only the statistical summary (without heavy data like positions/signals/candles) 4. Frontend can display backtest performance metrics when viewing a bot ## Database Schema ```sql ALTER TABLE "Bots" ADD COLUMN "BacktestId" integer NULL; ``` All changes follow the project's architecture patterns (Controller → Application → Repository) and maintain backward compatibility through nullable BacktestId fields.
1106 lines
38 KiB
C#
1106 lines
38 KiB
C#
using Exilion.TradingAtomics;
|
|
using Managing.Domain.Accounts;
|
|
using Managing.Domain.Backtests;
|
|
using Managing.Domain.Bots;
|
|
using Managing.Domain.Candles;
|
|
using Managing.Domain.Indicators;
|
|
using Managing.Domain.MoneyManagements;
|
|
using Managing.Domain.Scenarios;
|
|
using Managing.Domain.Statistics;
|
|
using Managing.Domain.Strategies;
|
|
using Managing.Domain.Trades;
|
|
using Managing.Domain.Users;
|
|
using Managing.Domain.Whitelist;
|
|
using Managing.Domain.Workers;
|
|
using Managing.Infrastructure.Databases.PostgreSql.Entities;
|
|
using Newtonsoft.Json;
|
|
using static Managing.Common.Enums;
|
|
using SystemJsonSerializer = System.Text.Json.JsonSerializer;
|
|
|
|
namespace Managing.Infrastructure.Databases.PostgreSql;
|
|
|
|
public static class PostgreSqlMappers
|
|
{
|
|
#region Account Mappings
|
|
|
|
public static Account Map(AccountEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new Account
|
|
{
|
|
Id = entity.Id,
|
|
Name = entity.Name,
|
|
Exchange = entity.Exchange,
|
|
Type = entity.Type,
|
|
Key = entity.Key,
|
|
Secret = entity.Secret,
|
|
User = entity.User != null ? Map(entity.User) : null,
|
|
Balances = new List<Balance>(), // Empty list for now, balances handled separately if needed
|
|
IsGmxInitialized = entity.IsGmxInitialized
|
|
};
|
|
}
|
|
|
|
public static AccountEntity Map(Account account)
|
|
{
|
|
if (account == null) return null;
|
|
|
|
return new AccountEntity
|
|
{
|
|
Id = account.Id,
|
|
Name = account.Name,
|
|
Exchange = account.Exchange,
|
|
Type = account.Type,
|
|
Key = account.Key,
|
|
Secret = account.Secret,
|
|
UserId = account.User.Id,
|
|
IsGmxInitialized = account.IsGmxInitialized
|
|
};
|
|
}
|
|
|
|
public static IEnumerable<Account> Map(IEnumerable<AccountEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<Account>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MoneyManagement Mappings
|
|
|
|
public static MoneyManagement Map(MoneyManagementEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new MoneyManagement
|
|
{
|
|
Name = entity.Name,
|
|
Timeframe = entity.Timeframe,
|
|
StopLoss = entity.StopLoss,
|
|
TakeProfit = entity.TakeProfit,
|
|
Leverage = entity.Leverage,
|
|
User = entity.User != null ? Map(entity.User) : null
|
|
};
|
|
}
|
|
|
|
public static MoneyManagementEntity Map(MoneyManagement moneyManagement)
|
|
{
|
|
if (moneyManagement == null) return null;
|
|
|
|
return new MoneyManagementEntity
|
|
{
|
|
Name = moneyManagement.Name,
|
|
Timeframe = moneyManagement.Timeframe,
|
|
StopLoss = moneyManagement.StopLoss,
|
|
TakeProfit = moneyManagement.TakeProfit,
|
|
Leverage = moneyManagement.Leverage,
|
|
UserId = moneyManagement.User?.Id ?? 0
|
|
};
|
|
}
|
|
|
|
public static IEnumerable<MoneyManagement> Map(IEnumerable<MoneyManagementEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<MoneyManagement>();
|
|
}
|
|
|
|
public static MoneyManagementEntity Map(LightMoneyManagement lightMoneyManagement)
|
|
{
|
|
if (lightMoneyManagement == null) return null;
|
|
|
|
return new MoneyManagementEntity
|
|
{
|
|
Name = lightMoneyManagement.Name,
|
|
Timeframe = lightMoneyManagement.Timeframe,
|
|
StopLoss = lightMoneyManagement.StopLoss,
|
|
TakeProfit = lightMoneyManagement.TakeProfit,
|
|
Leverage = lightMoneyManagement.Leverage
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region User Mappings
|
|
|
|
public static User Map(UserEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new User
|
|
{
|
|
Name = entity.Name,
|
|
AgentName = entity.AgentName,
|
|
AvatarUrl = entity.AvatarUrl,
|
|
TelegramChannel = entity.TelegramChannel,
|
|
OwnerWalletAddress = entity.OwnerWalletAddress,
|
|
Id = entity.Id, // Assuming Id is the primary key for UserEntity
|
|
IsAdmin = entity.IsAdmin,
|
|
LastConnectionDate = entity.LastConnectionDate,
|
|
LowEthAmountAlert = entity.LowEthAmountAlert,
|
|
EnableAutoswap = entity.EnableAutoswap,
|
|
AutoswapAmount = entity.AutoswapAmount,
|
|
MaxWaitingTimeForPositionToGetFilledSeconds = entity.MaxWaitingTimeForPositionToGetFilledSeconds,
|
|
MaxTxnGasFeePerPosition = entity.MaxTxnGasFeePerPosition,
|
|
IsGmxEnabled = entity.IsGmxEnabled,
|
|
GmxSlippage = entity.GmxSlippage,
|
|
MinimumConfidence = entity.MinimumConfidence,
|
|
TrendStrongAgreementThreshold = entity.TrendStrongAgreementThreshold,
|
|
SignalAgreementThreshold = entity.SignalAgreementThreshold,
|
|
AllowSignalTrendOverride = entity.AllowSignalTrendOverride,
|
|
DefaultExchange = entity.DefaultExchange,
|
|
DefaultLlmProvider = entity.DefaultLlmProvider,
|
|
Accounts = entity.Accounts?.Select(MapAccountWithoutUser).ToList() ?? new List<Account>()
|
|
};
|
|
}
|
|
|
|
// Helper method to map AccountEntity without User to avoid circular reference
|
|
private static Account MapAccountWithoutUser(AccountEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new Account
|
|
{
|
|
Id = entity.Id,
|
|
Name = entity.Name,
|
|
Exchange = entity.Exchange,
|
|
Type = entity.Type,
|
|
Key = entity.Key,
|
|
Secret = entity.Secret,
|
|
User = null, // Don't map User to avoid circular reference
|
|
Balances = new List<Balance>(), // Empty list for now, balances handled separately if needed
|
|
IsGmxInitialized = entity.IsGmxInitialized
|
|
};
|
|
}
|
|
|
|
public static UserEntity Map(User user)
|
|
{
|
|
if (user == null) return null;
|
|
|
|
return new UserEntity
|
|
{
|
|
Name = user.Name,
|
|
AgentName = user.AgentName,
|
|
AvatarUrl = user.AvatarUrl,
|
|
TelegramChannel = user.TelegramChannel,
|
|
OwnerWalletAddress = user.OwnerWalletAddress,
|
|
IsAdmin = user.IsAdmin,
|
|
LastConnectionDate = user.LastConnectionDate,
|
|
LowEthAmountAlert = user.LowEthAmountAlert,
|
|
EnableAutoswap = user.EnableAutoswap,
|
|
AutoswapAmount = user.AutoswapAmount,
|
|
MaxWaitingTimeForPositionToGetFilledSeconds = user.MaxWaitingTimeForPositionToGetFilledSeconds,
|
|
MaxTxnGasFeePerPosition = user.MaxTxnGasFeePerPosition,
|
|
IsGmxEnabled = user.IsGmxEnabled,
|
|
GmxSlippage = user.GmxSlippage,
|
|
MinimumConfidence = user.MinimumConfidence,
|
|
TrendStrongAgreementThreshold = user.TrendStrongAgreementThreshold,
|
|
SignalAgreementThreshold = user.SignalAgreementThreshold,
|
|
AllowSignalTrendOverride = user.AllowSignalTrendOverride,
|
|
DefaultExchange = user.DefaultExchange,
|
|
DefaultLlmProvider = user.DefaultLlmProvider
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GeneticRequest Mappings
|
|
|
|
public static GeneticRequest Map(GeneticRequestEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
var geneticRequest = new GeneticRequest(entity.RequestId)
|
|
{
|
|
User = entity.User != null ? Map(entity.User) : null,
|
|
CreatedAt = entity.CreatedAt,
|
|
CompletedAt = entity.CompletedAt,
|
|
Status = Enum.Parse<GeneticRequestStatus>(entity.Status),
|
|
Ticker = entity.Ticker,
|
|
Timeframe = entity.Timeframe,
|
|
StartDate = entity.StartDate,
|
|
EndDate = entity.EndDate,
|
|
Balance = entity.Balance,
|
|
PopulationSize = entity.PopulationSize,
|
|
Generations = entity.Generations,
|
|
MutationRate = entity.MutationRate,
|
|
SelectionMethod = entity.SelectionMethod,
|
|
CrossoverMethod = entity.CrossoverMethod,
|
|
MutationMethod = entity.MutationMethod,
|
|
ElitismPercentage = entity.ElitismPercentage,
|
|
MaxTakeProfit = entity.MaxTakeProfit,
|
|
BestFitness = entity.BestFitness,
|
|
BestIndividual = entity.BestIndividual,
|
|
ErrorMessage = entity.ErrorMessage,
|
|
ProgressInfo = entity.ProgressInfo,
|
|
BestChromosome = entity.BestChromosome,
|
|
BestFitnessSoFar = entity.BestFitnessSoFar,
|
|
CurrentGeneration = entity.CurrentGeneration
|
|
};
|
|
|
|
// Deserialize EligibleIndicators from JSON
|
|
if (!string.IsNullOrEmpty(entity.EligibleIndicatorsJson))
|
|
{
|
|
try
|
|
{
|
|
geneticRequest.EligibleIndicators =
|
|
SystemJsonSerializer.Deserialize<List<IndicatorType>>(entity.EligibleIndicatorsJson) ??
|
|
new List<IndicatorType>();
|
|
}
|
|
catch
|
|
{
|
|
geneticRequest.EligibleIndicators = new List<IndicatorType>();
|
|
}
|
|
}
|
|
|
|
return geneticRequest;
|
|
}
|
|
|
|
public static GeneticRequestEntity Map(GeneticRequest geneticRequest)
|
|
{
|
|
if (geneticRequest == null) return null;
|
|
|
|
var entity = new GeneticRequestEntity
|
|
{
|
|
RequestId = geneticRequest.RequestId,
|
|
UserId = geneticRequest.User?.Id ?? 0,
|
|
CreatedAt = geneticRequest.CreatedAt,
|
|
CompletedAt = geneticRequest.CompletedAt,
|
|
UpdatedAt = DateTime.UtcNow,
|
|
Status = geneticRequest.Status.ToString(),
|
|
Ticker = geneticRequest.Ticker,
|
|
Timeframe = geneticRequest.Timeframe,
|
|
StartDate = geneticRequest.StartDate,
|
|
EndDate = geneticRequest.EndDate,
|
|
Balance = geneticRequest.Balance,
|
|
PopulationSize = geneticRequest.PopulationSize,
|
|
Generations = geneticRequest.Generations,
|
|
MutationRate = geneticRequest.MutationRate,
|
|
SelectionMethod = geneticRequest.SelectionMethod,
|
|
CrossoverMethod = geneticRequest.CrossoverMethod,
|
|
MutationMethod = geneticRequest.MutationMethod,
|
|
ElitismPercentage = geneticRequest.ElitismPercentage,
|
|
MaxTakeProfit = geneticRequest.MaxTakeProfit,
|
|
BestFitness = geneticRequest.BestFitness,
|
|
BestIndividual = geneticRequest.BestIndividual,
|
|
ErrorMessage = geneticRequest.ErrorMessage,
|
|
ProgressInfo = geneticRequest.ProgressInfo,
|
|
BestChromosome = geneticRequest.BestChromosome,
|
|
BestFitnessSoFar = geneticRequest.BestFitnessSoFar,
|
|
CurrentGeneration = geneticRequest.CurrentGeneration
|
|
};
|
|
|
|
// Serialize EligibleIndicators to JSON
|
|
if (geneticRequest.EligibleIndicators != null && geneticRequest.EligibleIndicators.Any())
|
|
{
|
|
try
|
|
{
|
|
entity.EligibleIndicatorsJson = SystemJsonSerializer.Serialize(geneticRequest.EligibleIndicators);
|
|
}
|
|
catch
|
|
{
|
|
entity.EligibleIndicatorsJson = "[]";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
entity.EligibleIndicatorsJson = "[]";
|
|
}
|
|
|
|
return entity;
|
|
}
|
|
|
|
public static IEnumerable<GeneticRequest> Map(IEnumerable<GeneticRequestEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<GeneticRequest>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Backtest Mappings
|
|
|
|
public static Backtest Map(BacktestEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
// Deserialize JSON fields using MongoMappers for compatibility
|
|
var config = JsonConvert.DeserializeObject<TradingBotConfig>(entity.ConfigJson);
|
|
var positionsList = JsonConvert.DeserializeObject<List<Position>>(entity.PositionsJson) ?? new List<Position>();
|
|
var positions = positionsList.ToDictionary(p => p.Identifier, p => p);
|
|
var signalsList = JsonConvert.DeserializeObject<List<LightSignal>>(entity.SignalsJson) ??
|
|
new List<LightSignal>();
|
|
var signals = signalsList.ToDictionary(s => s.Identifier, s => s);
|
|
var statistics = !string.IsNullOrEmpty(entity.StatisticsJson)
|
|
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)
|
|
: null;
|
|
|
|
var backtest = new Backtest(config, positions, signals)
|
|
{
|
|
Id = entity.Identifier,
|
|
FinalPnl = entity.FinalPnl,
|
|
WinRate = entity.WinRate,
|
|
GrowthPercentage = entity.GrowthPercentage,
|
|
HodlPercentage = entity.HodlPercentage,
|
|
StartDate = entity.StartDate,
|
|
EndDate = entity.EndDate,
|
|
User = entity.User != null ? Map(entity.User) : null,
|
|
Statistics = statistics,
|
|
Fees = entity.Fees,
|
|
Score = entity.Score,
|
|
ScoreMessage = entity.ScoreMessage,
|
|
RequestId = entity.RequestId,
|
|
Metadata = entity.Metadata,
|
|
InitialBalance = entity.InitialBalance,
|
|
NetPnl = entity.NetPnl,
|
|
PositionCount = entity.PositionCount
|
|
};
|
|
|
|
return backtest;
|
|
}
|
|
|
|
public static BacktestEntity Map(Backtest backtest)
|
|
{
|
|
if (backtest == null) return null;
|
|
|
|
// Configure JSON serializer to handle circular references
|
|
var jsonSettings = new JsonSerializerSettings
|
|
{
|
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
|
PreserveReferencesHandling = PreserveReferencesHandling.None
|
|
};
|
|
|
|
return new BacktestEntity
|
|
{
|
|
Identifier = backtest.Id,
|
|
RequestId = backtest.RequestId,
|
|
FinalPnl = backtest.FinalPnl,
|
|
WinRate = backtest.WinRate,
|
|
GrowthPercentage = backtest.GrowthPercentage,
|
|
HodlPercentage = backtest.HodlPercentage,
|
|
ConfigJson = JsonConvert.SerializeObject(backtest.Config, jsonSettings),
|
|
Name = backtest.Config?.Name ?? string.Empty,
|
|
Ticker = backtest.Config?.Ticker.ToString() ?? string.Empty,
|
|
Timeframe = (int)backtest.Config.Timeframe,
|
|
TradingType = (int)backtest.Config.TradingType,
|
|
IndicatorsCsv = string.Join(',', backtest.Config.Scenario.Indicators.Select(i => i.Type.ToString())),
|
|
IndicatorsCount = backtest.Config.Scenario.Indicators.Count,
|
|
PositionsJson = JsonConvert.SerializeObject(backtest.Positions.Values.ToList(), jsonSettings),
|
|
SignalsJson = JsonConvert.SerializeObject(backtest.Signals.Values.ToList(), jsonSettings),
|
|
StartDate = backtest.StartDate,
|
|
EndDate = backtest.EndDate,
|
|
Duration = backtest.EndDate - backtest.StartDate,
|
|
MoneyManagementJson = JsonConvert.SerializeObject(backtest.Config?.MoneyManagement, jsonSettings),
|
|
UserId = backtest.User?.Id ?? 0,
|
|
StatisticsJson = backtest.Statistics != null
|
|
? JsonConvert.SerializeObject(backtest.Statistics, jsonSettings)
|
|
: null,
|
|
SharpeRatio = backtest.Statistics?.SharpeRatio ?? 0m,
|
|
MaxDrawdown = backtest.Statistics?.MaxDrawdown ?? 0m,
|
|
MaxDrawdownRecoveryTime = backtest.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero,
|
|
Fees = backtest.Fees,
|
|
Score = backtest.Score,
|
|
ScoreMessage = backtest.ScoreMessage ?? string.Empty,
|
|
Metadata = backtest.Metadata?.ToString(),
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow,
|
|
InitialBalance = backtest.InitialBalance,
|
|
NetPnl = backtest.NetPnl,
|
|
PositionCount = backtest.PositionCount
|
|
};
|
|
}
|
|
|
|
public static IEnumerable<Backtest> Map(IEnumerable<BacktestEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<Backtest>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region BundleBacktestRequest Mappings
|
|
|
|
public static BundleBacktestRequest Map(BundleBacktestRequestEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
var bundleRequest = new BundleBacktestRequest(entity.RequestId)
|
|
{
|
|
User = entity.User != null ? Map(entity.User) : null,
|
|
CreatedAt = entity.CreatedAt,
|
|
CompletedAt = entity.CompletedAt,
|
|
UpdatedAt = entity.UpdatedAt,
|
|
Status = entity.Status,
|
|
UniversalConfigJson = entity.UniversalConfigJson,
|
|
DateTimeRangesJson = entity.DateTimeRangesJson,
|
|
MoneyManagementVariantsJson = entity.MoneyManagementVariantsJson,
|
|
TickerVariantsJson = entity.TickerVariantsJson,
|
|
TotalBacktests = entity.TotalBacktests,
|
|
CompletedBacktests = entity.CompletedBacktests,
|
|
FailedBacktests = entity.FailedBacktests,
|
|
ErrorMessage = entity.ErrorMessage,
|
|
ProgressInfo = entity.ProgressInfo,
|
|
CurrentBacktest = entity.CurrentBacktest,
|
|
EstimatedTimeRemainingSeconds = entity.EstimatedTimeRemainingSeconds,
|
|
Name = entity.Name,
|
|
Version = entity.Version
|
|
};
|
|
|
|
// Deserialize Results from JSON
|
|
if (!string.IsNullOrEmpty(entity.ResultsJson))
|
|
{
|
|
try
|
|
{
|
|
bundleRequest.Results = JsonConvert.DeserializeObject<List<string>>(entity.ResultsJson) ??
|
|
new List<string>();
|
|
}
|
|
catch
|
|
{
|
|
bundleRequest.Results = new List<string>();
|
|
}
|
|
}
|
|
|
|
return bundleRequest;
|
|
}
|
|
|
|
public static BundleBacktestRequestEntity Map(BundleBacktestRequest bundleRequest)
|
|
{
|
|
if (bundleRequest == null) return null;
|
|
|
|
var entity = new BundleBacktestRequestEntity
|
|
{
|
|
RequestId = bundleRequest.RequestId,
|
|
UserId = bundleRequest.User?.Id ?? 0,
|
|
CreatedAt = bundleRequest.CreatedAt,
|
|
CompletedAt = bundleRequest.CompletedAt,
|
|
Status = bundleRequest.Status,
|
|
UniversalConfigJson = bundleRequest.UniversalConfigJson,
|
|
DateTimeRangesJson = bundleRequest.DateTimeRangesJson,
|
|
MoneyManagementVariantsJson = bundleRequest.MoneyManagementVariantsJson,
|
|
TickerVariantsJson = bundleRequest.TickerVariantsJson,
|
|
TotalBacktests = bundleRequest.TotalBacktests,
|
|
CompletedBacktests = bundleRequest.CompletedBacktests,
|
|
FailedBacktests = bundleRequest.FailedBacktests,
|
|
ErrorMessage = bundleRequest.ErrorMessage,
|
|
ProgressInfo = bundleRequest.ProgressInfo,
|
|
CurrentBacktest = bundleRequest.CurrentBacktest,
|
|
EstimatedTimeRemainingSeconds = bundleRequest.EstimatedTimeRemainingSeconds,
|
|
Name = bundleRequest.Name,
|
|
Version = bundleRequest.Version,
|
|
UpdatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
// Serialize Results to JSON
|
|
if (bundleRequest.Results != null && bundleRequest.Results.Any())
|
|
{
|
|
try
|
|
{
|
|
entity.ResultsJson = JsonConvert.SerializeObject(bundleRequest.Results);
|
|
}
|
|
catch
|
|
{
|
|
entity.ResultsJson = "[]";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
entity.ResultsJson = "[]";
|
|
}
|
|
|
|
return entity;
|
|
}
|
|
|
|
public static IEnumerable<BundleBacktestRequest> Map(IEnumerable<BundleBacktestRequestEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<BundleBacktestRequest>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Trading Mappings
|
|
|
|
// Scenario mappings
|
|
public static Scenario Map(ScenarioEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new Scenario(entity.Name, entity.LoopbackPeriod)
|
|
{
|
|
User = entity.User != null ? Map(entity.User) : null,
|
|
Indicators = new List<IndicatorBase>() // Will be populated separately when needed
|
|
};
|
|
}
|
|
|
|
public static ScenarioEntity Map(Scenario scenario)
|
|
{
|
|
if (scenario == null) return null;
|
|
|
|
return new ScenarioEntity
|
|
{
|
|
Name = scenario.Name,
|
|
LoopbackPeriod = scenario.LookbackPeriod,
|
|
UserId = scenario.User?.Id ?? 0
|
|
};
|
|
}
|
|
|
|
// Indicator mappings
|
|
public static IndicatorBase Map(IndicatorEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new IndicatorBase(entity.Name, entity.Type)
|
|
{
|
|
SignalType = entity.SignalType,
|
|
MinimumHistory = entity.MinimumHistory,
|
|
Period = entity.Period,
|
|
FastPeriods = entity.FastPeriods,
|
|
SlowPeriods = entity.SlowPeriods,
|
|
SignalPeriods = entity.SignalPeriods,
|
|
Multiplier = entity.Multiplier,
|
|
SmoothPeriods = entity.SmoothPeriods,
|
|
StochPeriods = entity.StochPeriods,
|
|
CyclePeriods = entity.CyclePeriods,
|
|
User = entity.User != null ? Map(entity.User) : null
|
|
};
|
|
}
|
|
|
|
public static IndicatorEntity Map(IndicatorBase indicatorBase)
|
|
{
|
|
if (indicatorBase == null) return null;
|
|
|
|
return new IndicatorEntity
|
|
{
|
|
Name = indicatorBase.Name,
|
|
Type = indicatorBase.Type,
|
|
Timeframe = Timeframe.FifteenMinutes, // Default timeframe
|
|
SignalType = indicatorBase.SignalType,
|
|
MinimumHistory = indicatorBase.MinimumHistory,
|
|
Period = indicatorBase.Period,
|
|
FastPeriods = indicatorBase.FastPeriods,
|
|
SlowPeriods = indicatorBase.SlowPeriods,
|
|
SignalPeriods = indicatorBase.SignalPeriods,
|
|
Multiplier = indicatorBase.Multiplier,
|
|
SmoothPeriods = indicatorBase.SmoothPeriods,
|
|
StochPeriods = indicatorBase.StochPeriods,
|
|
CyclePeriods = indicatorBase.CyclePeriods,
|
|
UserId = indicatorBase.User?.Id ?? 0
|
|
};
|
|
}
|
|
|
|
// Signal mappings
|
|
public static Signal Map(SignalEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
var candle = !string.IsNullOrEmpty(entity.CandleJson)
|
|
? JsonConvert.DeserializeObject<Candle>(entity.CandleJson)
|
|
: null;
|
|
|
|
return new Signal(
|
|
entity.Ticker,
|
|
entity.Direction,
|
|
entity.Confidence,
|
|
candle,
|
|
entity.Date,
|
|
TradingExchanges.Evm, // Default exchange
|
|
entity.Type,
|
|
entity.SignalType,
|
|
entity.IndicatorName,
|
|
entity.User != null ? Map(entity.User) : null)
|
|
{
|
|
Status = entity.Status
|
|
};
|
|
}
|
|
|
|
public static SignalEntity Map(Signal signal)
|
|
{
|
|
if (signal == null) return null;
|
|
|
|
return new SignalEntity
|
|
{
|
|
Identifier = signal.Identifier,
|
|
Direction = signal.Direction,
|
|
Confidence = signal.Confidence,
|
|
Date = signal.Date,
|
|
Ticker = signal.Ticker,
|
|
Status = signal.Status,
|
|
Timeframe = signal.Timeframe,
|
|
Type = signal.IndicatorType,
|
|
SignalType = signal.SignalType,
|
|
IndicatorName = signal.IndicatorName,
|
|
UserId = signal.User?.Id ?? 0,
|
|
CandleJson = signal.Candle != null ? JsonConvert.SerializeObject(signal.Candle) : null
|
|
};
|
|
}
|
|
|
|
// Position mappings
|
|
public static Position Map(PositionEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
// Deserialize money management
|
|
var moneyManagement = new MoneyManagement(); // Default money management
|
|
if (!string.IsNullOrEmpty(entity.MoneyManagementJson))
|
|
{
|
|
moneyManagement = JsonConvert.DeserializeObject<MoneyManagement>(entity.MoneyManagementJson) ??
|
|
new MoneyManagement();
|
|
}
|
|
|
|
var position = new Position(
|
|
entity.Identifier,
|
|
entity.AccountId,
|
|
entity.OriginDirection,
|
|
entity.Ticker,
|
|
moneyManagement,
|
|
entity.Initiator,
|
|
entity.Date,
|
|
entity.User != null ? Map(entity.User) : null)
|
|
{
|
|
Status = entity.Status,
|
|
SignalIdentifier = entity.SignalIdentifier,
|
|
InitiatorIdentifier = entity.InitiatorIdentifier,
|
|
TradingType = entity.TradingType
|
|
};
|
|
|
|
// Set ProfitAndLoss with proper type
|
|
position.ProfitAndLoss = new ProfitAndLoss { Realized = entity.ProfitAndLoss, Net = entity.NetPnL };
|
|
|
|
// Set fee properties
|
|
position.UiFees = entity.UiFees;
|
|
position.GasFees = entity.GasFees;
|
|
|
|
// Map related trades
|
|
if (entity.OpenTrade != null)
|
|
position.Open = Map(entity.OpenTrade);
|
|
if (entity.StopLossTrade != null)
|
|
position.StopLoss = Map(entity.StopLossTrade);
|
|
if (entity.TakeProfit1Trade != null)
|
|
position.TakeProfit1 = Map(entity.TakeProfit1Trade);
|
|
if (entity.TakeProfit2Trade != null)
|
|
position.TakeProfit2 = Map(entity.TakeProfit2Trade);
|
|
|
|
return position;
|
|
}
|
|
|
|
public static PositionEntity Map(Position position)
|
|
{
|
|
if (position == null) return null;
|
|
|
|
return new PositionEntity
|
|
{
|
|
Identifier = position.Identifier,
|
|
Date = position.Date,
|
|
AccountId = position.AccountId,
|
|
ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0,
|
|
UiFees = position.UiFees,
|
|
GasFees = position.GasFees,
|
|
OriginDirection = position.OriginDirection,
|
|
Status = position.Status,
|
|
Ticker = position.Ticker,
|
|
Initiator = position.Initiator,
|
|
SignalIdentifier = position.SignalIdentifier,
|
|
UserId = position.User?.Id ?? 0,
|
|
InitiatorIdentifier = position.InitiatorIdentifier,
|
|
TradingType = position.TradingType,
|
|
MoneyManagementJson = position.MoneyManagement != null
|
|
? JsonConvert.SerializeObject(position.MoneyManagement)
|
|
: null,
|
|
NetPnL = position.ProfitAndLoss?.Net ??
|
|
(position.ProfitAndLoss?.Realized - position.UiFees - position.GasFees ?? 0)
|
|
};
|
|
}
|
|
|
|
// Trade mappings
|
|
public static Trade Map(TradeEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new Trade(
|
|
entity.Date,
|
|
entity.Direction,
|
|
entity.Status,
|
|
entity.TradeType,
|
|
entity.Ticker,
|
|
entity.Quantity,
|
|
entity.Price,
|
|
entity.Leverage,
|
|
entity.ExchangeOrderId,
|
|
entity.Message);
|
|
}
|
|
|
|
public static TradeEntity Map(Trade trade)
|
|
{
|
|
if (trade == null) return null;
|
|
|
|
return new TradeEntity
|
|
{
|
|
Date = trade.Date,
|
|
Direction = trade.Direction,
|
|
Status = trade.Status,
|
|
TradeType = trade.TradeType,
|
|
Ticker = trade.Ticker,
|
|
Quantity = trade.Quantity,
|
|
Price = trade.Price,
|
|
Leverage = trade.Leverage,
|
|
ExchangeOrderId = trade.ExchangeOrderId,
|
|
Message = trade.Message
|
|
};
|
|
}
|
|
|
|
|
|
// Collection mappings
|
|
public static IEnumerable<Scenario> Map(IEnumerable<ScenarioEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<Scenario>();
|
|
}
|
|
|
|
public static IEnumerable<IndicatorBase> Map(IEnumerable<IndicatorEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<IndicatorBase>();
|
|
}
|
|
|
|
public static IEnumerable<Signal> Map(IEnumerable<SignalEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<Signal>();
|
|
}
|
|
|
|
public static IEnumerable<Position> Map(IEnumerable<PositionEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<Position>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Bot Mappings
|
|
|
|
// BotBackup mappings
|
|
public static Bot Map(BotEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
var bot = new Bot
|
|
{
|
|
Identifier = entity.Identifier,
|
|
User = entity.User != null ? Map(entity.User) : null,
|
|
Status = entity.Status,
|
|
CreateDate = entity.CreateDate,
|
|
Name = entity.Name,
|
|
Ticker = entity.Ticker,
|
|
TradingType = entity.TradingType,
|
|
StartupTime = entity.StartupTime,
|
|
LastStartTime = entity.LastStartTime,
|
|
LastStopTime = entity.LastStopTime,
|
|
AccumulatedRunTimeSeconds = entity.AccumulatedRunTimeSeconds,
|
|
TradeWins = entity.TradeWins,
|
|
TradeLosses = entity.TradeLosses,
|
|
Pnl = entity.Pnl,
|
|
NetPnL = entity.NetPnL,
|
|
Roi = entity.Roi,
|
|
Volume = entity.Volume,
|
|
Fees = entity.Fees,
|
|
LongPositionCount = entity.LongPositionCount,
|
|
ShortPositionCount = entity.ShortPositionCount,
|
|
BotTradingBalance = entity.BotTradingBalance,
|
|
BacktestId = entity.BacktestId,
|
|
MasterBotUserId = entity.MasterBotUserId,
|
|
MasterBotUser = entity.MasterBotUser != null ? Map(entity.MasterBotUser) : null
|
|
};
|
|
|
|
return bot;
|
|
}
|
|
|
|
public static BotEntity Map(Bot bot)
|
|
{
|
|
if (bot == null) return null;
|
|
|
|
return new BotEntity
|
|
{
|
|
Identifier = bot.Identifier,
|
|
UserId = bot.User.Id,
|
|
Status = bot.Status,
|
|
CreateDate = bot.CreateDate,
|
|
Name = bot.Name,
|
|
Ticker = bot.Ticker,
|
|
TradingType = bot.TradingType,
|
|
StartupTime = bot.StartupTime,
|
|
LastStartTime = bot.LastStartTime,
|
|
LastStopTime = bot.LastStopTime,
|
|
AccumulatedRunTimeSeconds = bot.AccumulatedRunTimeSeconds,
|
|
TradeWins = bot.TradeWins,
|
|
TradeLosses = bot.TradeLosses,
|
|
Pnl = bot.Pnl,
|
|
NetPnL = bot.NetPnL,
|
|
Roi = bot.Roi,
|
|
Volume = bot.Volume,
|
|
Fees = bot.Fees,
|
|
LongPositionCount = bot.LongPositionCount,
|
|
ShortPositionCount = bot.ShortPositionCount,
|
|
BotTradingBalance = bot.BotTradingBalance,
|
|
BacktestId = bot.BacktestId,
|
|
MasterBotUserId = bot.MasterBotUserId,
|
|
UpdatedAt = DateTime.UtcNow
|
|
};
|
|
}
|
|
|
|
public static IEnumerable<Bot> Map(IEnumerable<BotEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<Bot>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Statistics Mappings
|
|
|
|
// TopVolumeTicker mappings
|
|
public static TopVolumeTicker Map(TopVolumeTickerEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new TopVolumeTicker
|
|
{
|
|
Ticker = entity.Ticker,
|
|
Date = entity.Date,
|
|
Volume = entity.Volume,
|
|
Rank = entity.Rank,
|
|
Exchange = entity.Exchange
|
|
};
|
|
}
|
|
|
|
public static TopVolumeTickerEntity Map(TopVolumeTicker topVolumeTicker)
|
|
{
|
|
if (topVolumeTicker == null) return null;
|
|
|
|
return new TopVolumeTickerEntity
|
|
{
|
|
Ticker = topVolumeTicker.Ticker,
|
|
Date = topVolumeTicker.Date,
|
|
Volume = topVolumeTicker.Volume,
|
|
Rank = topVolumeTicker.Rank,
|
|
Exchange = topVolumeTicker.Exchange
|
|
};
|
|
}
|
|
|
|
public static IEnumerable<TopVolumeTicker> Map(IEnumerable<TopVolumeTickerEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<TopVolumeTicker>();
|
|
}
|
|
|
|
// SpotlightOverview mappings
|
|
public static SpotlightOverview Map(SpotlightOverviewEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
var overview = new SpotlightOverview
|
|
{
|
|
Identifier = entity.Identifier,
|
|
DateTime = entity.DateTime,
|
|
ScenarioCount = entity.ScenarioCount,
|
|
Spotlights = new List<Spotlight>()
|
|
};
|
|
|
|
// Deserialize the JSON spotlights data
|
|
if (!string.IsNullOrEmpty(entity.SpotlightsJson))
|
|
{
|
|
try
|
|
{
|
|
overview.Spotlights = SystemJsonSerializer.Deserialize<List<Spotlight>>(entity.SpotlightsJson) ??
|
|
new List<Spotlight>();
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// If deserialization fails, return empty list
|
|
overview.Spotlights = new List<Spotlight>();
|
|
}
|
|
}
|
|
|
|
return overview;
|
|
}
|
|
|
|
public static SpotlightOverviewEntity Map(SpotlightOverview overview)
|
|
{
|
|
if (overview == null) return null;
|
|
|
|
var entity = new SpotlightOverviewEntity
|
|
{
|
|
Identifier = overview.Identifier,
|
|
DateTime = overview.DateTime,
|
|
ScenarioCount = overview.ScenarioCount
|
|
};
|
|
|
|
// Serialize the spotlights to JSON
|
|
if (overview.Spotlights != null)
|
|
{
|
|
entity.SpotlightsJson = SystemJsonSerializer.Serialize(overview.Spotlights);
|
|
}
|
|
|
|
return entity;
|
|
}
|
|
|
|
public static IEnumerable<SpotlightOverview> Map(IEnumerable<SpotlightOverviewEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<SpotlightOverview>();
|
|
}
|
|
|
|
// Trader mappings
|
|
public static Trader Map(TraderEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new Trader
|
|
{
|
|
Address = entity.Address,
|
|
Winrate = entity.Winrate,
|
|
Pnl = entity.Pnl,
|
|
TradeCount = entity.TradeCount,
|
|
AverageWin = entity.AverageWin,
|
|
AverageLoss = entity.AverageLoss,
|
|
Roi = entity.Roi
|
|
};
|
|
}
|
|
|
|
public static TraderEntity Map(Trader trader, bool isBestTrader)
|
|
{
|
|
if (trader == null) return null;
|
|
|
|
return new TraderEntity
|
|
{
|
|
Address = trader.Address,
|
|
Winrate = trader.Winrate,
|
|
Pnl = trader.Pnl,
|
|
TradeCount = trader.TradeCount,
|
|
AverageWin = trader.AverageWin,
|
|
AverageLoss = trader.AverageLoss,
|
|
Roi = trader.Roi,
|
|
IsBestTrader = isBestTrader
|
|
};
|
|
}
|
|
|
|
public static IEnumerable<Trader> Map(IEnumerable<TraderEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<Trader>();
|
|
}
|
|
|
|
// FundingRate mappings
|
|
public static FundingRate Map(FundingRateEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new FundingRate
|
|
{
|
|
Ticker = entity.Ticker,
|
|
Exchange = entity.Exchange,
|
|
Rate = entity.Rate,
|
|
OpenInterest = entity.OpenInterest,
|
|
Date = entity.Date,
|
|
Direction = entity.Direction
|
|
};
|
|
}
|
|
|
|
public static FundingRateEntity Map(FundingRate fundingRate)
|
|
{
|
|
if (fundingRate == null) return null;
|
|
|
|
return new FundingRateEntity
|
|
{
|
|
Ticker = fundingRate.Ticker,
|
|
Exchange = fundingRate.Exchange,
|
|
Rate = fundingRate.Rate,
|
|
OpenInterest = fundingRate.OpenInterest,
|
|
Date = fundingRate.Date,
|
|
Direction = fundingRate.Direction
|
|
};
|
|
}
|
|
|
|
public static IEnumerable<FundingRate> Map(IEnumerable<FundingRateEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<FundingRate>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Worker Mappings
|
|
|
|
public static Worker Map(WorkerEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
return new Worker
|
|
{
|
|
WorkerType = entity.WorkerType,
|
|
StartTime = entity.StartTime,
|
|
LastRunTime = entity.LastRunTime,
|
|
ExecutionCount = entity.ExecutionCount,
|
|
Delay = TimeSpan.FromTicks(entity.DelayTicks),
|
|
IsActive = entity.IsActive
|
|
};
|
|
}
|
|
|
|
public static WorkerEntity Map(Worker worker)
|
|
{
|
|
if (worker == null) return null;
|
|
return new WorkerEntity
|
|
{
|
|
WorkerType = worker.WorkerType,
|
|
StartTime = worker.StartTime,
|
|
LastRunTime = worker.LastRunTime,
|
|
ExecutionCount = worker.ExecutionCount,
|
|
DelayTicks = worker.Delay.Ticks,
|
|
IsActive = worker.IsActive
|
|
};
|
|
}
|
|
|
|
public static IEnumerable<Worker> Map(IEnumerable<WorkerEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<Worker>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region WhitelistAccount Mappings
|
|
|
|
public static WhitelistAccount Map(WhitelistAccountEntity entity)
|
|
{
|
|
if (entity == null) return null;
|
|
|
|
return new WhitelistAccount
|
|
{
|
|
Id = entity.Id,
|
|
PrivyId = entity.PrivyId,
|
|
PrivyCreationDate = entity.PrivyCreationDate,
|
|
EmbeddedWallet = entity.EmbeddedWallet,
|
|
ExternalEthereumAccount = entity.ExternalEthereumAccount,
|
|
TwitterAccount = entity.TwitterAccount,
|
|
IsWhitelisted = entity.IsWhitelisted,
|
|
CreatedAt = entity.CreatedAt,
|
|
UpdatedAt = entity.UpdatedAt
|
|
};
|
|
}
|
|
|
|
public static WhitelistAccountEntity Map(WhitelistAccount whitelistAccount)
|
|
{
|
|
if (whitelistAccount == null) return null;
|
|
|
|
return new WhitelistAccountEntity
|
|
{
|
|
Id = whitelistAccount.Id,
|
|
PrivyId = whitelistAccount.PrivyId,
|
|
PrivyCreationDate = whitelistAccount.PrivyCreationDate,
|
|
EmbeddedWallet = whitelistAccount.EmbeddedWallet,
|
|
ExternalEthereumAccount = whitelistAccount.ExternalEthereumAccount,
|
|
TwitterAccount = whitelistAccount.TwitterAccount,
|
|
IsWhitelisted = whitelistAccount.IsWhitelisted,
|
|
CreatedAt = whitelistAccount.CreatedAt,
|
|
UpdatedAt = whitelistAccount.UpdatedAt
|
|
};
|
|
}
|
|
|
|
public static IEnumerable<WhitelistAccount> Map(IEnumerable<WhitelistAccountEntity> entities)
|
|
{
|
|
return entities?.Select(Map) ?? Enumerable.Empty<WhitelistAccount>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
private static int? ExtractBundleIndex(string name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name)) return null;
|
|
var hashIndex = name.LastIndexOf('#');
|
|
if (hashIndex < 0 || hashIndex + 1 >= name.Length) return null;
|
|
var numberPart = name.Substring(hashIndex + 1).Trim();
|
|
return int.TryParse(numberPart, out var n) ? n : (int?)null;
|
|
}
|
|
} |