Files
managing-apps/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs
cryptooda 6f55566db3 Implement LLM provider configuration and update user settings
- Added functionality to update the default LLM provider for users via a new endpoint in UserController.
- Introduced LlmProvider enum to manage available LLM options: Auto, Gemini, OpenAI, and Claude.
- Updated User and UserEntity models to include DefaultLlmProvider property.
- Enhanced database context and migrations to support the new LLM provider configuration.
- Integrated LLM services into the application bootstrap for dependency injection.
- Updated TypeScript API client to include methods for managing LLM providers and chat requests.
2026-01-03 21:55:55 +07:00

821 lines
36 KiB
C#

using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class ManagingDbContext : DbContext
{
private readonly ILogger<ManagingDbContext>? _logger;
private readonly SentrySqlMonitoringService? _sentryMonitoringService;
private readonly Dictionary<string, int> _queryExecutionCounts = new();
private readonly object _queryCountLock = new object();
public ManagingDbContext(DbContextOptions<ManagingDbContext> options) : base(options)
{
}
public ManagingDbContext(DbContextOptions<ManagingDbContext> options, ILogger<ManagingDbContext> logger, SentrySqlMonitoringService sentryMonitoringService)
: base(options)
{
_logger = logger;
_sentryMonitoringService = sentryMonitoringService;
}
public DbSet<AccountEntity> Accounts { get; set; }
public DbSet<UserEntity> Users { get; set; }
public DbSet<GeneticRequestEntity> GeneticRequests { get; set; }
public DbSet<BacktestEntity> Backtests { get; set; }
public DbSet<BundleBacktestRequestEntity> BundleBacktestRequests { get; set; }
public DbSet<JobEntity> Jobs { get; set; }
// Trading entities
public DbSet<ScenarioEntity> Scenarios { get; set; }
public DbSet<IndicatorEntity> Indicators { get; set; }
public DbSet<ScenarioIndicatorEntity> ScenarioIndicators { get; set; }
public DbSet<SignalEntity> Signals { get; set; }
public DbSet<PositionEntity> Positions { get; set; }
public DbSet<TradeEntity> Trades { get; set; }
// Statistics entities
public DbSet<TopVolumeTickerEntity> TopVolumeTickers { get; set; }
public DbSet<SpotlightOverviewEntity> SpotlightOverviews { get; set; }
public DbSet<TraderEntity> Traders { get; set; }
public DbSet<FundingRateEntity> FundingRates { get; set; }
public DbSet<AgentSummaryEntity> AgentSummaries { get; set; }
// Bot entities
public DbSet<BotEntity> Bots { get; set; }
// Settings entities
public DbSet<MoneyManagementEntity> MoneyManagements { get; set; }
// Worker entities
public DbSet<WorkerEntity> Workers { get; set; }
public DbSet<SynthMinersLeaderboardEntity> SynthMinersLeaderboards { get; set; }
public DbSet<SynthPredictionEntity> SynthPredictions { get; set; }
public DbSet<WhitelistAccountEntity> WhitelistAccounts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure schema for Orleans tables (if needed for future organization)
// Orleans tables will remain in the default schema for now
// This can be changed later if needed by configuring specific schemas
// Configure Account entity
modelBuilder.Entity<AccountEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Key).HasMaxLength(500);
entity.Property(e => e.Secret).HasMaxLength(500);
entity.Property(e => e.Exchange)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.Type)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.IsGmxInitialized)
.IsRequired()
.HasDefaultValue(false); // Default value for new records
// Create unique index on account name
entity.HasIndex(e => e.Name).IsUnique();
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany(u => u.Accounts)
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
});
// Configure User entity
modelBuilder.Entity<UserEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.AgentName).HasMaxLength(255);
entity.Property(e => e.AvatarUrl).HasMaxLength(500);
entity.Property(e => e.TelegramChannel).HasMaxLength(255);
entity.Property(e => e.MinimumConfidence)
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.DefaultExchange)
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.DefaultLlmProvider)
.HasConversion<string>() // Store enum as string
.HasDefaultValueSql("'Auto'"); // Default LLM provider
// Create indexes for performance
entity.HasIndex(e => e.Name).IsUnique();
entity.HasIndex(e => e.AgentName);
});
// Configure GeneticRequest entity
modelBuilder.Entity<GeneticRequestEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.RequestId).IsRequired().HasMaxLength(255);
entity.Property(e => e.Status).IsRequired().HasMaxLength(50);
entity.Property(e => e.Ticker)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.Timeframe)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.SelectionMethod)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.CrossoverMethod)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.MutationMethod)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.Balance).HasColumnType("decimal(18,8)");
entity.Property(e => e.BestIndividual).HasMaxLength(4000);
entity.Property(e => e.ErrorMessage).HasMaxLength(2000);
entity.Property(e => e.ProgressInfo).HasMaxLength(4000);
entity.Property(e => e.BestChromosome).HasMaxLength(4000);
entity.Property(e => e.EligibleIndicatorsJson).HasMaxLength(2000);
// Create indexes
entity.HasIndex(e => e.RequestId).IsUnique();
entity.HasIndex(e => e.Status);
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
});
// Configure Backtest entity
modelBuilder.Entity<BacktestEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255);
entity.Property(e => e.RequestId).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserId).IsRequired();
entity.Property(e => e.FinalPnl).HasColumnType("decimal(18,8)");
entity.Property(e => e.GrowthPercentage).HasColumnType("decimal(18,8)");
entity.Property(e => e.HodlPercentage).HasColumnType("decimal(18,8)");
entity.Property(e => e.Fees).HasColumnType("decimal(18,8)");
entity.Property(e => e.ConfigJson).HasColumnType("jsonb");
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Ticker).HasMaxLength(32);
entity.Property(e => e.Timeframe).IsRequired();
entity.Property(e => e.TradingType).IsRequired();
entity.Property(e => e.IndicatorsCsv).HasColumnType("text");
entity.Property(e => e.IndicatorsCount).IsRequired();
entity.Property(e => e.PositionsJson).HasColumnType("jsonb");
entity.Property(e => e.SignalsJson).HasColumnType("jsonb");
entity.Property(e => e.MoneyManagementJson).HasColumnType("jsonb");
entity.Property(e => e.StatisticsJson).HasColumnType("jsonb");
entity.Property(e => e.SharpeRatio).HasColumnType("decimal(18,8)").HasDefaultValue(0m);
entity.Property(e => e.MaxDrawdown).HasColumnType("decimal(18,8)").HasDefaultValue(0m);
entity.Property(e => e.MaxDrawdownRecoveryTime).HasDefaultValue(TimeSpan.Zero);
entity.Property(e => e.Duration).HasDefaultValue(TimeSpan.Zero);
entity.Property(e => e.ScoreMessage).HasMaxLength(1000);
entity.Property(e => e.Metadata).HasColumnType("text");
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Create indexes for common queries
entity.HasIndex(e => e.Identifier).IsUnique();
entity.HasIndex(e => e.RequestId);
entity.HasIndex(e => e.UserId);
entity.HasIndex(e => e.Score);
// Composite indexes for efficient pagination and filtering
entity.HasIndex(e => new { e.UserId, e.Score });
entity.HasIndex(e => new { e.UserId, e.Name });
entity.HasIndex(e => new { e.RequestId, e.Score });
entity.HasIndex(e => new { e.UserId, e.Ticker });
entity.HasIndex(e => new { e.UserId, e.Timeframe });
});
// Configure BundleBacktestRequest entity
modelBuilder.Entity<BundleBacktestRequestEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.RequestId).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserId);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Version).IsRequired().HasDefaultValue(1);
entity.Property(e => e.Status)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.UniversalConfigJson).HasColumnType("text");
entity.Property(e => e.DateTimeRangesJson).HasColumnType("text");
entity.Property(e => e.MoneyManagementVariantsJson).HasColumnType("text");
entity.Property(e => e.TickerVariantsJson).HasColumnType("text");
entity.Property(e => e.ErrorMessage).HasColumnType("text");
entity.Property(e => e.ProgressInfo).HasColumnType("text");
entity.Property(e => e.CurrentBacktest).HasMaxLength(500);
entity.Property(e => e.ResultsJson).HasColumnType("jsonb");
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Create indexes for common queries
entity.HasIndex(e => e.RequestId).IsUnique();
entity.HasIndex(e => e.UserId);
entity.HasIndex(e => e.Status);
// Composite index for user queries ordered by creation date
entity.HasIndex(e => new { e.UserId, e.CreatedAt });
// Composite index for user queries by name and version
entity.HasIndex(e => new { e.UserId, e.Name, e.Version });
});
// Configure BacktestJob entity
modelBuilder.Entity<JobEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).ValueGeneratedNever(); // GUIDs are generated by application
entity.Property(e => e.UserId).IsRequired();
entity.Property(e => e.Status).IsRequired();
entity.Property(e => e.JobType).IsRequired().HasDefaultValue(0); // 0 = Backtest
entity.Property(e => e.Priority).IsRequired().HasDefaultValue(0);
entity.Property(e => e.ConfigJson).IsRequired().HasColumnType("jsonb");
entity.Property(e => e.StartDate).IsRequired();
entity.Property(e => e.EndDate).IsRequired();
entity.Property(e => e.ProgressPercentage).IsRequired().HasDefaultValue(0);
entity.Property(e => e.AssignedWorkerId).HasMaxLength(255);
entity.Property(e => e.ResultJson).HasColumnType("jsonb");
entity.Property(e => e.ErrorMessage).HasColumnType("text");
entity.Property(e => e.RequestId).HasMaxLength(255);
entity.Property(e => e.GeneticRequestId).HasMaxLength(255);
entity.Property(e => e.CreatedAt).IsRequired();
// Indexes for efficient job claiming and queries
entity.HasIndex(e => new { e.Status, e.JobType, e.Priority, e.CreatedAt })
.HasDatabaseName("idx_status_jobtype_priority_created");
entity.HasIndex(e => e.BundleRequestId)
.HasDatabaseName("idx_bundle_request");
entity.HasIndex(e => new { e.AssignedWorkerId, e.Status })
.HasDatabaseName("idx_assigned_worker");
entity.HasIndex(e => new { e.UserId, e.Status })
.HasDatabaseName("idx_user_status");
entity.HasIndex(e => e.GeneticRequestId)
.HasDatabaseName("idx_genetic_request");
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Explicitly set table name to "Jobs" (uppercase)
entity.ToTable("Jobs");
});
// Configure Scenario entity
modelBuilder.Entity<ScenarioEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserId).IsRequired();
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Create indexes
entity.HasIndex(e => e.UserId);
// Composite index for user scenarios
entity.HasIndex(e => new { e.UserId, e.Name });
});
// Configure Indicator entity
modelBuilder.Entity<IndicatorEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Type).IsRequired().HasConversion<string>();
entity.Property(e => e.Timeframe).IsRequired().HasConversion<string>();
entity.Property(e => e.SignalType).IsRequired().HasConversion<string>();
entity.Property(e => e.UserId);
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Create indexes
entity.HasIndex(e => e.UserId);
// Composite index for user indicators
entity.HasIndex(e => new { e.UserId, e.Name });
});
// Configure ScenarioIndicator junction table
modelBuilder.Entity<ScenarioIndicatorEntity>(entity =>
{
entity.HasKey(e => e.Id);
// Configure relationships
entity.HasOne(e => e.Scenario)
.WithMany(s => s.ScenarioIndicators)
.HasForeignKey(e => e.ScenarioId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Indicator)
.WithMany(i => i.ScenarioIndicators)
.HasForeignKey(e => e.IndicatorId)
.OnDelete(DeleteBehavior.Cascade);
// Create indexes
entity.HasIndex(e => new { e.ScenarioId, e.IndicatorId }).IsUnique();
});
// Configure Signal entity
modelBuilder.Entity<SignalEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255);
entity.Property(e => e.Direction).IsRequired().HasConversion<string>();
entity.Property(e => e.Confidence).IsRequired().HasConversion<string>();
entity.Property(e => e.Ticker).IsRequired().HasConversion<string>();
entity.Property(e => e.Status).IsRequired().HasConversion<string>();
entity.Property(e => e.Timeframe).IsRequired().HasConversion<string>();
entity.Property(e => e.Type).IsRequired().HasConversion<string>();
entity.Property(e => e.SignalType).IsRequired().HasConversion<string>();
entity.Property(e => e.IndicatorName).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserId);
entity.Property(e => e.CandleJson).HasColumnType("text");
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Create indexes
entity.HasIndex(e => e.Identifier);
entity.HasIndex(e => e.UserId);
entity.HasIndex(e => e.Date);
entity.HasIndex(e => e.Ticker);
entity.HasIndex(e => e.Status);
// Composite indexes for common queries
entity.HasIndex(e => new { e.UserId, e.Date });
entity.HasIndex(e => new { e.Identifier, e.Date, e.UserId }).IsUnique();
});
// Configure Position entity
modelBuilder.Entity<PositionEntity>(entity =>
{
entity.HasKey(e => e.Identifier);
entity.Property(e => e.ProfitAndLoss).HasColumnType("decimal(18,8)");
entity.Property(e => e.NetPnL).HasColumnType("decimal(18,8)");
entity.Property(e => e.OriginDirection).IsRequired().HasConversion<string>();
entity.Property(e => e.Status).IsRequired().HasConversion<string>();
entity.Property(e => e.Ticker).IsRequired().HasConversion<string>();
entity.Property(e => e.Initiator).IsRequired().HasConversion<string>();
entity.Property(e => e.SignalIdentifier).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserId);
entity.Property(e => e.InitiatorIdentifier).IsRequired();
entity.Property(e => e.MoneyManagementJson).HasColumnType("text");
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Configure relationships with trades
entity.HasOne(e => e.OpenTrade)
.WithMany()
.HasForeignKey(e => e.OpenTradeId)
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne(e => e.StopLossTrade)
.WithMany()
.HasForeignKey(e => e.StopLossTradeId)
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne(e => e.TakeProfit1Trade)
.WithMany()
.HasForeignKey(e => e.TakeProfit1TradeId)
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne(e => e.TakeProfit2Trade)
.WithMany()
.HasForeignKey(e => e.TakeProfit2TradeId)
.OnDelete(DeleteBehavior.SetNull);
// Create indexes
entity.HasIndex(e => e.Identifier).IsUnique();
entity.HasIndex(e => e.UserId);
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.InitiatorIdentifier);
// Composite indexes
entity.HasIndex(e => new { e.UserId, e.Identifier });
});
// Configure Trade entity
modelBuilder.Entity<TradeEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Direction).IsRequired().HasConversion<string>();
entity.Property(e => e.Status).IsRequired().HasConversion<string>();
entity.Property(e => e.TradeType).IsRequired().HasConversion<string>();
entity.Property(e => e.Ticker).IsRequired().HasConversion<string>();
entity.Property(e => e.Quantity).HasColumnType("decimal(18,8)");
entity.Property(e => e.Price).HasColumnType("decimal(18,8)");
entity.Property(e => e.Leverage).HasColumnType("decimal(18,8)");
entity.Property(e => e.ExchangeOrderId).HasMaxLength(255);
entity.Property(e => e.Message).HasColumnType("text");
// Create indexes
entity.HasIndex(e => e.Date);
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.ExchangeOrderId);
});
// Configure TopVolumeTicker entity
modelBuilder.Entity<TopVolumeTickerEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Volume).HasPrecision(18, 8);
// Create indexes
entity.HasIndex(e => e.Ticker);
entity.HasIndex(e => e.Date);
entity.HasIndex(e => e.Exchange);
// Composite indexes for efficient queries
entity.HasIndex(e => new { e.Exchange, e.Date });
entity.HasIndex(e => new { e.Date, e.Rank });
});
// Configure SpotlightOverview entity
modelBuilder.Entity<SpotlightOverviewEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Identifier).IsRequired();
entity.Property(e => e.SpotlightsJson).HasColumnType("jsonb");
// Create indexes
entity.HasIndex(e => e.Identifier).IsUnique();
entity.HasIndex(e => e.DateTime);
// Composite index for efficient queries
entity.HasIndex(e => new { e.DateTime, e.ScenarioCount });
});
// Configure Trader entity
modelBuilder.Entity<TraderEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Address).IsRequired().HasMaxLength(255);
entity.Property(e => e.Pnl).HasPrecision(18, 8);
entity.Property(e => e.AverageWin).HasPrecision(18, 8);
entity.Property(e => e.AverageLoss).HasPrecision(18, 8);
entity.Property(e => e.Roi).HasPrecision(18, 8);
// Create indexes
entity.HasIndex(e => e.Address);
entity.HasIndex(e => e.IsBestTrader);
entity.HasIndex(e => e.Winrate);
entity.HasIndex(e => e.Pnl);
entity.HasIndex(e => e.Roi);
// Composite indexes for efficient queries
entity.HasIndex(e => new { e.IsBestTrader, e.Winrate });
entity.HasIndex(e => new { e.IsBestTrader, e.Roi });
entity.HasIndex(e => new { e.Address, e.IsBestTrader }).IsUnique();
});
// Configure FundingRate entity
modelBuilder.Entity<FundingRateEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Rate).HasPrecision(18, 8);
entity.Property(e => e.OpenInterest).HasPrecision(18, 8);
// Create indexes
entity.HasIndex(e => e.Ticker);
entity.HasIndex(e => e.Exchange);
entity.HasIndex(e => e.Date);
// Composite indexes for efficient queries
entity.HasIndex(e => new { e.Ticker, e.Exchange });
entity.HasIndex(e => new { e.Exchange, e.Date });
entity.HasIndex(e => new { e.Ticker, e.Exchange, e.Date }).IsUnique();
});
// Configure BotBackup entity
modelBuilder.Entity<BotEntity>(entity =>
{
entity.HasKey(e => e.Identifier);
entity.Property(e => e.Identifier).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.CreateDate).IsRequired();
entity.Property(e => e.StartupTime).IsRequired();
// Runtime tracking fields
entity.Property(e => e.LastStartTime);
entity.Property(e => e.LastStopTime);
entity.Property(e => e.AccumulatedRunTimeSeconds);
entity.Property(e => e.TradeWins).IsRequired();
entity.Property(e => e.TradeLosses).IsRequired();
entity.Property(e => e.Pnl).HasPrecision(18, 8);
entity.Property(e => e.NetPnL).HasPrecision(18, 8);
entity.Property(e => e.Roi).HasPrecision(18, 8);
entity.Property(e => e.Volume).HasPrecision(18, 8);
entity.Property(e => e.Fees).HasPrecision(18, 8);
entity.Property(e => e.LongPositionCount).IsRequired();
entity.Property(e => e.ShortPositionCount).IsRequired();
// Create indexes
entity.HasIndex(e => e.Identifier).IsUnique();
entity.HasIndex(e => e.Status);
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Configure relationship with MasterBotUser
entity.HasOne(e => e.MasterBotUser)
.WithMany()
.HasForeignKey(e => e.MasterBotUserId)
.OnDelete(DeleteBehavior.SetNull);
});
// Configure MoneyManagement entity
modelBuilder.Entity<MoneyManagementEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Timeframe).IsRequired().HasConversion<string>();
entity.Property(e => e.StopLoss).HasColumnType("decimal(18,8)");
entity.Property(e => e.TakeProfit).HasColumnType("decimal(18,8)");
entity.Property(e => e.Leverage).HasColumnType("decimal(18,8)");
entity.Property(e => e.UserName).HasMaxLength(255);
// Create indexes
entity.HasIndex(e => e.UserName);
// Composite index for user money managements
entity.HasIndex(e => new { e.UserName, e.Name });
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
});
// Configure Worker entity
modelBuilder.Entity<WorkerEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.WorkerType).IsRequired().HasConversion<string>();
entity.Property(e => e.StartTime).IsRequired();
entity.Property(e => e.LastRunTime);
entity.Property(e => e.ExecutionCount).IsRequired();
entity.Property(e => e.DelayTicks).IsRequired();
entity.Property(e => e.IsActive).IsRequired();
entity.HasIndex(e => e.WorkerType).IsUnique();
});
// Configure SynthMinersLeaderboard entity
modelBuilder.Entity<SynthMinersLeaderboardEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Asset).IsRequired().HasMaxLength(32);
entity.Property(e => e.TimeIncrement).IsRequired();
entity.Property(e => e.SignalDate);
entity.Property(e => e.IsBacktest).IsRequired();
entity.Property(e => e.MinersData).HasColumnType("jsonb");
entity.Property(e => e.CacheKey).IsRequired().HasMaxLength(255);
entity.Property(e => e.CreatedAt).IsRequired();
entity.HasIndex(e => e.CacheKey).IsUnique();
});
// Configure SynthPrediction entity
modelBuilder.Entity<SynthPredictionEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Asset).IsRequired().HasMaxLength(32);
entity.Property(e => e.MinerUid).IsRequired();
entity.Property(e => e.TimeIncrement).IsRequired();
entity.Property(e => e.TimeLength).IsRequired();
entity.Property(e => e.SignalDate);
entity.Property(e => e.IsBacktest).IsRequired();
entity.Property(e => e.PredictionData).HasColumnType("jsonb");
entity.Property(e => e.CacheKey).IsRequired().HasMaxLength(255);
entity.Property(e => e.CreatedAt).IsRequired();
entity.HasIndex(e => e.CacheKey).IsUnique();
});
// Configure WhitelistAccount entity
modelBuilder.Entity<WhitelistAccountEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.PrivyId).IsRequired().HasMaxLength(255);
entity.Property(e => e.PrivyCreationDate).IsRequired();
entity.Property(e => e.EmbeddedWallet).IsRequired().HasMaxLength(42);
entity.Property(e => e.ExternalEthereumAccount).HasMaxLength(42);
entity.Property(e => e.TwitterAccount).HasMaxLength(255);
entity.Property(e => e.IsWhitelisted)
.IsRequired()
.HasDefaultValue(false);
entity.Property(e => e.CreatedAt).IsRequired();
entity.Property(e => e.UpdatedAt);
// Create indexes for search performance
entity.HasIndex(e => e.PrivyId).IsUnique();
entity.HasIndex(e => e.EmbeddedWallet).IsUnique();
entity.HasIndex(e => e.ExternalEthereumAccount);
entity.HasIndex(e => e.TwitterAccount);
entity.HasIndex(e => e.CreatedAt);
});
// Configure AgentSummary entity
modelBuilder.Entity<AgentSummaryEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.UserId).IsRequired();
entity.Property(e => e.AgentName).IsRequired().HasMaxLength(255);
entity.Property(e => e.TotalPnL).HasColumnType("decimal(18,8)");
entity.Property(e => e.TotalROI).HasColumnType("decimal(18,8)");
entity.Property(e => e.Wins).IsRequired();
entity.Property(e => e.Losses).IsRequired();
entity.Property(e => e.Runtime);
entity.Property(e => e.CreatedAt).IsRequired();
entity.Property(e => e.UpdatedAt).IsRequired();
entity.Property(e => e.ActiveStrategiesCount).IsRequired();
entity.Property(e => e.TotalVolume).HasPrecision(18, 8);
entity.Property(e => e.TotalBalance).HasPrecision(18, 8);
entity.Property(e => e.TotalFees).HasPrecision(18, 8);
entity.Property(e => e.NetPnL).HasPrecision(18, 8);
entity.Property(e => e.BacktestCount).IsRequired();
// Create indexes for common queries
entity.HasIndex(e => e.UserId).IsUnique();
entity.HasIndex(e => e.AgentName).IsUnique();
entity.HasIndex(e => e.TotalPnL);
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
}
/// <summary>
/// Ensures Orleans tables are properly initialized in the database.
/// This method can be called during application startup to verify Orleans infrastructure.
/// </summary>
public async Task EnsureOrleansTablesExistAsync()
{
// Orleans tables are automatically created by the Orleans framework
// when using AdoNetClustering and AdoNetReminderService.
// This method serves as a verification point and can be extended
// for custom Orleans table management if needed.
// For now, we just ensure the database is accessible
await Database.CanConnectAsync();
}
/// <summary>
/// Gets Orleans table statistics for monitoring purposes.
/// This helps track Orleans table sizes and performance.
/// </summary>
public async Task<Dictionary<string, long>> GetOrleansTableStatsAsync()
{
var stats = new Dictionary<string, long>();
// Orleans table names
var orleansTables = new[]
{
"orleansmembershiptable",
"orleansmembershipversiontable",
"orleansquery",
"orleansreminderstable",
"orleansstorage"
};
foreach (var tableName in orleansTables)
{
try
{
var count = await Database.SqlQueryRaw<long>($"SELECT COUNT(*) FROM \"{tableName}\"").FirstOrDefaultAsync();
stats[tableName] = count;
}
catch
{
// Table might not exist yet (normal during startup)
stats[tableName] = -1;
}
}
return stats;
}
/// <summary>
/// Database organization strategy:
/// - Application tables: Default schema (public)
/// - Orleans tables: Default schema (public) - managed by Orleans framework
/// - Future consideration: Move Orleans tables to 'orleans' schema if needed
///
/// Benefits of current approach:
/// - Single database simplifies deployment and backup
/// - Orleans tables are automatically managed by the framework
/// - No additional configuration complexity
/// - Easier monitoring and maintenance
/// </summary>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
// Add any additional configuration here if needed
}
/// <summary>
/// Tracks query execution for loop detection and performance monitoring
/// </summary>
/// <param name="queryPattern">Pattern or hash of the query</param>
/// <param name="executionTime">Time taken to execute the query</param>
/// <param name="repositoryName">Name of the repository executing the query</param>
/// <param name="methodName">Name of the method executing the query</param>
public void TrackQueryExecution(string queryPattern, TimeSpan executionTime, string repositoryName, string methodName)
{
if (_logger == null || _sentryMonitoringService == null) return;
// Track execution count for this query pattern
lock (_queryCountLock)
{
_queryExecutionCounts[queryPattern] = _queryExecutionCounts.GetValueOrDefault(queryPattern, 0) + 1;
}
// Check for potential loops with Sentry integration
var isLoopDetected = _sentryMonitoringService.TrackQueryExecution(repositoryName, methodName, queryPattern, executionTime);
// Only log query execution details if it should be logged based on monitoring settings
if (_sentryMonitoringService.ShouldLogQuery(executionTime))
{
_logger.LogWarning(
"[SQL-QUERY-TRACKED] {Repository}.{Method} | Pattern: {Pattern} | Time: {Time}ms | Count: {Count}",
repositoryName, methodName, queryPattern, executionTime.TotalMilliseconds,
_queryExecutionCounts[queryPattern]);
}
// Alert on potential loops
if (isLoopDetected)
{
_logger.LogError(
"[SQL-LOOP-ALERT] Potential infinite loop detected in {Repository}.{Method} with pattern '{Pattern}'",
repositoryName, methodName, queryPattern);
}
}
/// <summary>
/// Gets current query execution statistics
/// </summary>
public Dictionary<string, int> GetQueryExecutionCounts()
{
lock (_queryCountLock)
{
return new Dictionary<string, int>(_queryExecutionCounts);
}
}
/// <summary>
/// Clears query execution tracking data
/// </summary>
public void ClearQueryTracking()
{
lock (_queryCountLock)
{
_queryExecutionCounts.Clear();
}
_logger?.LogInformation("[SQL-TRACKING] Query execution counts cleared");
}
}