diff --git a/src/Managing.Api/Controllers/AccountController.cs b/src/Managing.Api/Controllers/AccountController.cs index 2099e0b6..f5952167 100644 --- a/src/Managing.Api/Controllers/AccountController.cs +++ b/src/Managing.Api/Controllers/AccountController.cs @@ -147,5 +147,18 @@ namespace Managing.Api.Controllers var user = await GetUser(); return Ok(_AccountService.DeleteAccount(user, name)); } + + /// + /// Retrieves the approval status for all supported trading exchanges for the authenticated user. + /// Returns a list showing each exchange with its initialization status (true/false). + /// + /// A list of exchange approval statuses. + [HttpGet("exchange-approval-status")] + public async Task>> GetExchangeApprovalStatus() + { + var user = await GetUser(); + var exchangeStatuses = await _AccountService.GetExchangeApprovalStatusAsync(user); + return Ok(exchangeStatuses); + } } } \ No newline at end of file diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index 8660a7cf..a6bfcca9 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -3,7 +3,6 @@ using Managing.Api.Models.Requests; using Managing.Api.Models.Responses; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; -using Managing.Application.Hubs; using Managing.Application.ManageBot.Commands; using Managing.Domain.Backtests; using Managing.Domain.Bots; @@ -16,7 +15,6 @@ using Managing.Domain.Trades; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; using static Managing.Common.Enums; namespace Managing.Api.Controllers; @@ -35,7 +33,6 @@ public class DataController : ControllerBase private readonly ICacheService _cacheService; private readonly IStatisticService _statisticService; private readonly IAgentService _agentService; - private readonly IHubContext _hubContext; private readonly IMediator _mediator; private readonly ITradingService _tradingService; private readonly IGrainFactory _grainFactory; @@ -58,7 +55,6 @@ public class DataController : ControllerBase ICacheService cacheService, IStatisticService statisticService, IAgentService agentService, - IHubContext hubContext, IMediator mediator, ITradingService tradingService, IGrainFactory grainFactory) @@ -68,7 +64,6 @@ public class DataController : ControllerBase _cacheService = cacheService; _statisticService = statisticService; _agentService = agentService; - _hubContext = hubContext; _mediator = mediator; _tradingService = tradingService; _grainFactory = grainFactory; diff --git a/src/Managing.Api/Controllers/TradingController.cs b/src/Managing.Api/Controllers/TradingController.cs index db5c1525..ea33b155 100644 --- a/src/Managing.Api/Controllers/TradingController.cs +++ b/src/Managing.Api/Controllers/TradingController.cs @@ -183,7 +183,7 @@ public class TradingController : BaseController return Forbid("You don't have permission to initialize this wallet address. You can only initialize your own wallet addresses."); } - var result = await _tradingService.InitPrivyWallet(publicAddress); + var result = await _tradingService.InitPrivyWallet(publicAddress, TradingExchanges.GmxV2); return Ok(result); } catch (Exception ex) diff --git a/src/Managing.Api/Program.cs b/src/Managing.Api/Program.cs index 34a76066..db6e6e23 100644 --- a/src/Managing.Api/Program.cs +++ b/src/Managing.Api/Program.cs @@ -313,7 +313,6 @@ app.UseEndpoints(endpoints => endpoints.MapControllers(); endpoints.MapHub("/bothub"); endpoints.MapHub("/backtesthub"); - endpoints.MapHub("/candlehub"); endpoints.MapHealthChecks("/health", new HealthCheckOptions { diff --git a/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs b/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs new file mode 100644 index 00000000..b252eb20 --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs @@ -0,0 +1,19 @@ +using Managing.Domain.Candles; +using Orleans; + +namespace Managing.Application.Abstractions.Grains; + +/// +/// Orleans grain interface for candle storage and retrieval. +/// This grain manages in-memory historical candle data with state persistence +/// and subscribes to price streams for real-time updates. +/// +public interface ICandleStoreGrain : IGrainWithStringKey +{ + /// + /// Gets the current list of historical candles (up to 500 most recent) + /// + /// List of candles ordered by date + Task> GetCandlesAsync(); +} + diff --git a/src/Managing.Application.Abstractions/Grains/IPriceFetcher1DayGrain.cs b/src/Managing.Application.Abstractions/Grains/IPriceFetcher1DayGrain.cs new file mode 100644 index 00000000..2cf71826 --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/IPriceFetcher1DayGrain.cs @@ -0,0 +1,19 @@ +using Orleans; + +namespace Managing.Application.Abstractions.Grains; + +/// +/// Orleans grain interface for daily price fetching operations. +/// This stateless worker grain handles fetching daily price data from external APIs +/// and publishing to Orleans streams. +/// +public interface IPriceFetcher1DayGrain : IGrainWithIntegerKey +{ + /// + /// Fetches daily price data for all supported exchange/ticker combinations + /// and publishes new candles to their respective streams. + /// + /// True if the operation completed successfully, false otherwise + Task FetchAndPublishPricesAsync(); +} + diff --git a/src/Managing.Application.Abstractions/Grains/IPriceFetcher1HourGrain.cs b/src/Managing.Application.Abstractions/Grains/IPriceFetcher1HourGrain.cs new file mode 100644 index 00000000..06396133 --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/IPriceFetcher1HourGrain.cs @@ -0,0 +1,19 @@ +using Orleans; + +namespace Managing.Application.Abstractions.Grains; + +/// +/// Orleans grain interface for 1-hour price fetching operations. +/// This stateless worker grain handles fetching 1-hour price data from external APIs +/// and publishing to Orleans streams. +/// +public interface IPriceFetcher1HourGrain : IGrainWithIntegerKey +{ + /// + /// Fetches 1-hour price data for all supported exchange/ticker combinations + /// and publishes new candles to their respective streams. + /// + /// True if the operation completed successfully, false otherwise + Task FetchAndPublishPricesAsync(); +} + diff --git a/src/Managing.Application.Abstractions/Grains/IPriceFetcher4HourGrain.cs b/src/Managing.Application.Abstractions/Grains/IPriceFetcher4HourGrain.cs new file mode 100644 index 00000000..ecbd9bf8 --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/IPriceFetcher4HourGrain.cs @@ -0,0 +1,19 @@ +using Orleans; + +namespace Managing.Application.Abstractions.Grains; + +/// +/// Orleans grain interface for 4-hour price fetching operations. +/// This stateless worker grain handles fetching 4-hour price data from external APIs +/// and publishing to Orleans streams. +/// +public interface IPriceFetcher4HourGrain : IGrainWithIntegerKey +{ + /// + /// Fetches 4-hour price data for all supported exchange/ticker combinations + /// and publishes new candles to their respective streams. + /// + /// True if the operation completed successfully, false otherwise + Task FetchAndPublishPricesAsync(); +} + diff --git a/src/Managing.Application.Abstractions/Grains/IPriceFetcher5MinGrain.cs b/src/Managing.Application.Abstractions/Grains/IPriceFetcher5MinGrain.cs new file mode 100644 index 00000000..04d7a693 --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/IPriceFetcher5MinGrain.cs @@ -0,0 +1,18 @@ +using Orleans; + +namespace Managing.Application.Abstractions.Grains; + +/// +/// Orleans grain interface for 5-minute price fetching operations. +/// This stateless worker grain handles fetching 5-minute price data from external APIs +/// and publishing to Orleans streams. +/// +public partial interface IPriceFetcher5MinGrain : IGrainWithIntegerKey +{ + /// + /// Fetches 5-minute price data for all supported exchange/ticker combinations + /// and publishes new candles to their respective streams. + /// + /// True if the operation completed successfully, false otherwise + Task FetchAndPublishPricesAsync(); +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Repositories/IAccountRepository.cs b/src/Managing.Application.Abstractions/Repositories/IAccountRepository.cs index f635bd64..743118e3 100644 --- a/src/Managing.Application.Abstractions/Repositories/IAccountRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/IAccountRepository.cs @@ -7,6 +7,7 @@ public interface IAccountRepository Task GetAccountByNameAsync(string name); Task GetAccountByKeyAsync(string key); Task InsertAccountAsync(Account account); + Task UpdateAccountAsync(Account account); void DeleteAccountByName(string name); Task> GetAccountsAsync(); } diff --git a/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs b/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs index dae00b0c..cd33025e 100644 --- a/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs +++ b/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs @@ -27,7 +27,7 @@ public interface IEvmManager decimal GetVolume(SubgraphProvider subgraphProvider, Ticker ticker); Task> GetAvailableTicker(); Task GetCandle(Ticker ticker); - Task InitAddress(string publicAddress); + Task InitAddressForGMX(string publicAddress); Task Send(Chain chain, Ticker ticker, decimal amount, string publicAddress, string privateKey, string receiverAddress); diff --git a/src/Managing.Application.Abstractions/Services/IAccountService.cs b/src/Managing.Application.Abstractions/Services/IAccountService.cs index e18a9216..1eaad354 100644 --- a/src/Managing.Application.Abstractions/Services/IAccountService.cs +++ b/src/Managing.Application.Abstractions/Services/IAccountService.cs @@ -34,4 +34,6 @@ public interface IAccountService Task SendTokenAsync(User user, string accountName, string recipientAddress, Ticker ticker, decimal amount, int? chainId = null); + + Task> GetExchangeApprovalStatusAsync(User user); } \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/IStreamService.cs b/src/Managing.Application.Abstractions/Services/IStreamService.cs deleted file mode 100644 index 8ef92c73..00000000 --- a/src/Managing.Application.Abstractions/Services/IStreamService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Managing.Application.Abstractions.Services; - -public interface IStreamService -{ - Task SubscribeCandle(); - Task UnSubscribeCandle(); -} diff --git a/src/Managing.Application.Abstractions/Services/ITradingService.cs b/src/Managing.Application.Abstractions/Services/ITradingService.cs index 81d8717f..59fde499 100644 --- a/src/Managing.Application.Abstractions/Services/ITradingService.cs +++ b/src/Managing.Application.Abstractions/Services/ITradingService.cs @@ -38,7 +38,7 @@ public interface ITradingService Task> GetAllDatabasePositionsAsync(); Task> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier); Task> GetPositionsByInitiatorIdentifiersAsync(IEnumerable initiatorIdentifiers); - Task InitPrivyWallet(string publicAddress); + Task InitPrivyWallet(string publicAddress, TradingExchanges tradingExchange); // Synth API integration methods Task ValidateSynthSignalAsync(LightSignal signal, decimal currentPrice, diff --git a/src/Managing.Application/Accounts/AccountService.cs b/src/Managing.Application/Accounts/AccountService.cs index c20baef2..3f8ff8b3 100644 --- a/src/Managing.Application/Accounts/AccountService.cs +++ b/src/Managing.Application/Accounts/AccountService.cs @@ -333,6 +333,34 @@ public class AccountService : IAccountService } } + public async Task> GetExchangeApprovalStatusAsync(User user) + { + var accounts = await GetAccountsByUserAsync(user, hideSecrets: true, getBalance: false); + + var exchangeStatuses = new List(); + + foreach (var account in accounts) + { + exchangeStatuses.Add(new ExchangeApprovalStatus + { + Exchange = TradingExchanges.GmxV2, + IsApproved = account.IsGmxInitialized + }); + } + + // Future: Add other exchanges here when supported + // e.g.: + // var hasEvmInitialized = accounts.Any(account => + // account.Exchange == TradingExchanges.Evm && account.IsGmxInitialized); + // exchangeStatuses.Add(new ExchangeApprovalStatus + // { + // Exchange = TradingExchanges.Evm, + // IsApproved = hasEvmInitialized + // }); + + return exchangeStatuses; + } + private async Task ManagePropertiesAsync(bool hideSecrets, bool getBalance, Account account) { if (account != null) diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index 6bf7bf55..82fa8595 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -210,7 +210,7 @@ public class TradingBotBase : ITradingBot Low = position.Open.Price, Volume = 0, Exchange = TradingExchanges.Evm, - Ticker = Config.Ticker.ToString(), + Ticker = Config.Ticker, Timeframe = Config.Timeframe }; diff --git a/src/Managing.Application/Grains/CandleStoreGrain.cs b/src/Managing.Application/Grains/CandleStoreGrain.cs new file mode 100644 index 00000000..96988477 --- /dev/null +++ b/src/Managing.Application/Grains/CandleStoreGrain.cs @@ -0,0 +1,215 @@ +using Managing.Application.Abstractions.Grains; +using Managing.Application.Abstractions.Repositories; +using Managing.Domain.Candles; +using Microsoft.Extensions.Logging; +using Orleans.Streams; +using static Managing.Common.Enums; + +namespace Managing.Application.Grains; + +/// +/// Grain for managing in-memory historical candle data with Orleans state persistence. +/// Subscribes to price streams and maintains a rolling window of 500 candles. +/// +public class CandleStoreGrain : Grain, ICandleStoreGrain, IAsyncObserver +{ + private readonly IPersistentState _state; + private readonly ILogger _logger; + private readonly ICandleRepository _candleRepository; + + private const int MaxCandleCount = 500; + private IAsyncStream _priceStream; + private StreamSubscriptionHandle _streamSubscription; + + public CandleStoreGrain( + [PersistentState("candle-store-state", "candle-store")] + IPersistentState state, + ILogger logger, + ICandleRepository candleRepository) + { + _state = state; + _logger = logger; + _candleRepository = candleRepository; + } + + public override async Task OnActivateAsync(CancellationToken cancellationToken) + { + var grainKey = this.GetPrimaryKeyString(); + _logger.LogInformation("CandleStoreGrain activated for key: {GrainKey}", grainKey); + + // Parse the grain key to extract exchange, ticker, and timeframe + var parts = grainKey.Split('-'); + if (parts.Length != 3) + { + _logger.LogError("Invalid grain key format: {GrainKey}. Expected format: Exchange-Ticker-Timeframe", grainKey); + return; + } + + if (!Enum.TryParse(parts[0], out var exchange) || + !Enum.TryParse(parts[1], out var ticker) || + !Enum.TryParse(parts[2], out var timeframe)) + { + _logger.LogError("Failed to parse grain key components: {GrainKey}", grainKey); + return; + } + + // Initialize state if empty + if (_state.State.Candles == null || _state.State.Candles.Count == 0) + { + await LoadInitialCandlesAsync(exchange, ticker, timeframe); + } + + // Subscribe to the price stream + await SubscribeToPriceStreamAsync(grainKey); + + await base.OnActivateAsync(cancellationToken); + } + + public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken) + { + // Unsubscribe from the stream + if (_streamSubscription != null) + { + await _streamSubscription.UnsubscribeAsync(); + _streamSubscription = null; + } + + await base.OnDeactivateAsync(reason, cancellationToken); + } + + public Task> GetCandlesAsync() + { + try + { + return Task.FromResult(_state.State.Candles?.ToList() ?? new List()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving candles for grain {GrainKey}", this.GetPrimaryKeyString()); + return Task.FromResult(new List()); + } + } + + // Stream observer implementation + public async Task OnNextAsync(Candle candle, StreamSequenceToken token = null) + { + try + { + _logger.LogDebug("Received new candle for {GrainKey} at {Date}", + this.GetPrimaryKeyString(), candle.Date); + + // Initialize state if needed + if (_state.State.Candles == null) + { + _state.State.Candles = new List(); + } + + // Add the new candle + _state.State.Candles.Add(candle); + + // Maintain rolling window of 500 candles + if (_state.State.Candles.Count > MaxCandleCount) + { + // Sort by date and keep the most recent 500 + _state.State.Candles = _state.State.Candles + .OrderBy(c => c.Date) + .TakeLast(MaxCandleCount) + .ToList(); + } + + // Persist the updated state + await _state.WriteStateAsync(); + + _logger.LogTrace("Updated candle store for {GrainKey}, total candles: {Count}", + this.GetPrimaryKeyString(), _state.State.Candles.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing new candle for grain {GrainKey}", this.GetPrimaryKeyString()); + } + } + + public Task OnCompletedAsync() + { + _logger.LogInformation("Stream completed for grain {GrainKey}", this.GetPrimaryKeyString()); + return Task.CompletedTask; + } + + public Task OnErrorAsync(Exception ex) + { + _logger.LogError(ex, "Stream error for grain {GrainKey}", this.GetPrimaryKeyString()); + return Task.CompletedTask; + } + + private async Task LoadInitialCandlesAsync(TradingExchanges exchange, Ticker ticker, Timeframe timeframe) + { + try + { + _logger.LogInformation("Loading initial candles for {Exchange}-{Ticker}-{Timeframe}", + exchange, ticker, timeframe); + + // Load the last 500 candles from the database + var endDate = DateTime.UtcNow; + var startDate = endDate.AddDays(-30); // Look back 30 days to ensure we get enough data + + var candles = await _candleRepository.GetCandles(exchange, ticker, timeframe, startDate, endDate, MaxCandleCount); + + if (candles?.Any() == true) + { + _state.State.Candles = candles + .OrderBy(c => c.Date) + .TakeLast(MaxCandleCount) + .ToList(); + + await _state.WriteStateAsync(); + + _logger.LogInformation("Loaded {Count} initial candles for {Exchange}-{Ticker}-{Timeframe}", + _state.State.Candles.Count, exchange, ticker, timeframe); + } + else + { + _state.State.Candles = new List(); + await _state.WriteStateAsync(); + + _logger.LogWarning("No initial candles found for {Exchange}-{Ticker}-{Timeframe}", + exchange, ticker, timeframe); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading initial candles for {Exchange}-{Ticker}-{Timeframe}", + exchange, ticker, timeframe); + + // Initialize empty state on error + _state.State.Candles = new List(); + await _state.WriteStateAsync(); + } + } + + private async Task SubscribeToPriceStreamAsync(string streamKey) + { + try + { + var streamProvider = this.GetStreamProvider("DefaultStreamProvider"); + _priceStream = streamProvider.GetStream(streamKey); + + _streamSubscription = await _priceStream.SubscribeAsync(this); + + _logger.LogInformation("Subscribed to price stream for {StreamKey}", streamKey); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error subscribing to price stream for {StreamKey}", streamKey); + } + } +} + +/// +/// State object for CandleStoreGrain containing the rolling window of candles +/// +[GenerateSerializer] +public class CandleStoreGrainState +{ + [Id(0)] + public List Candles { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Managing.Application/Grains/PriceFetcher5MinGrain.cs b/src/Managing.Application/Grains/PriceFetcher5MinGrain.cs new file mode 100644 index 00000000..77b90b7d --- /dev/null +++ b/src/Managing.Application/Grains/PriceFetcher5MinGrain.cs @@ -0,0 +1,172 @@ +using Managing.Application.Abstractions.Grains; +using Managing.Application.Abstractions.Repositories; +using Managing.Application.Abstractions.Services; +using Managing.Common; +using Managing.Domain.Accounts; +using Managing.Domain.Candles; +using Microsoft.Extensions.Logging; +using Orleans.Concurrency; +using Orleans.Streams; +using static Managing.Common.Enums; + +namespace Managing.Application.Grains; + +/// +/// StatelessWorker grain for fetching 5-minute price data from external APIs and publishing to Orleans streams. +/// This grain runs every 5 minutes and processes all exchange/ticker combinations for the 5-minute timeframe. +/// +[StatelessWorker] +public class PriceFetcher5MinGrain : Grain, IPriceFetcher5MinGrain, IRemindable +{ + private readonly ILogger _logger; + private readonly IExchangeService _exchangeService; + private readonly ICandleRepository _candleRepository; + private readonly IGrainFactory _grainFactory; + + private const string FetchPricesReminderName = "FetchPricesReminder"; + + // Predefined lists of trading parameters to fetch + private static readonly TradingExchanges[] SupportedExchanges = + { + TradingExchanges.GmxV2 + }; + + private static readonly Ticker[] SupportedTickers = Constants.GMX.Config.SupportedTickers; + + private static readonly Timeframe TargetTimeframe = Timeframe.FiveMinutes; + + public PriceFetcher5MinGrain( + ILogger logger, + IExchangeService exchangeService, + ICandleRepository candleRepository, + IGrainFactory grainFactory) + { + _logger = logger; + _exchangeService = exchangeService; + _candleRepository = candleRepository; + _grainFactory = grainFactory; + } + + public override async Task OnActivateAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("PriceFetcher5MinGrain activated"); + + // Register a reminder to fetch prices every 5 minutes + await this.RegisterOrUpdateReminder( + FetchPricesReminderName, + TimeSpan.FromMinutes(5), + TimeSpan.FromMinutes(5)); + + await base.OnActivateAsync(cancellationToken); + } + + public async Task FetchAndPublishPricesAsync() + { + try + { + _logger.LogInformation("Starting 5-minute price fetch cycle"); + + var fetchTasks = new List(); + + // Create fetch tasks for all exchange/ticker combinations for 5-minute timeframe + foreach (var exchange in SupportedExchanges) + { + foreach (var ticker in SupportedTickers) + { + fetchTasks.Add(FetchAndPublish(exchange, ticker, TargetTimeframe)); + } + } + + // Execute all fetch operations in parallel + await Task.WhenAll(fetchTasks); + + _logger.LogInformation("Completed 5-minute price fetch cycle for {TotalCombinations} combinations", + fetchTasks.Count); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during price fetch cycle"); + return false; + } + } + + private async Task FetchAndPublish(TradingExchanges exchange, Ticker ticker, Timeframe timeframe) + { + try + { + // Create a dummy account for API calls (this may need to be adjusted based on your implementation) + var account = new Account + { + Name = "PriceFetcher", + Exchange = exchange, + Type = AccountType.Watch + }; + + // Get the last candle date from database + var existingCandles = await _candleRepository.GetCandles(exchange, ticker, timeframe, + DateTime.UtcNow.AddDays(-7), 1); + + var startDate = existingCandles.Any() + ? existingCandles.Max(c => c.Date).AddMinutes(GetTimeframeMinutes(timeframe)) + : DateTime.UtcNow.AddDays(-1); + + // Fetch new candles from external API + var newCandles = await _exchangeService.GetCandles(account, ticker, startDate, timeframe, true); + + if (newCandles?.Any() == true) + { + var streamProvider = this.GetStreamProvider("DefaultStreamProvider"); + var streamKey = $"{exchange}-{ticker}-{timeframe}"; + var stream = streamProvider.GetStream(streamKey); + + _logger.LogDebug("Fetched {CandleCount} new candles for {StreamKey}", + newCandles.Count, streamKey); + + // Process each new candle + foreach (var candle in newCandles.OrderBy(c => c.Date)) + { + // Ensure candle has correct metadata + candle.Exchange = exchange; + candle.Ticker = ticker; + candle.Timeframe = timeframe; + + // Save to database + await _candleRepository.InsertCandle(candle); + + // Publish to stream + await stream.OnNextAsync(candle); + + _logger.LogTrace("Published candle for {StreamKey} at {Date}", + streamKey, candle.Date); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching prices for {Exchange}-{Ticker}-{Timeframe}", + exchange, ticker, timeframe); + } + } + + private static int GetTimeframeMinutes(Timeframe timeframe) => timeframe switch + { + Timeframe.OneMinute => 1, + Timeframe.FiveMinutes => 5, + Timeframe.FifteenMinutes => 15, + Timeframe.ThirtyMinutes => 30, + Timeframe.OneHour => 60, + Timeframe.FourHour => 240, + Timeframe.OneDay => 1440, + _ => 1 + }; + + public async Task ReceiveReminder(string reminderName, TickStatus status) + { + if (reminderName == FetchPricesReminderName) + { + await FetchAndPublishPricesAsync(); + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/Grains/PriceFetcherInitializer.cs b/src/Managing.Application/Grains/PriceFetcherInitializer.cs new file mode 100644 index 00000000..18cdf812 --- /dev/null +++ b/src/Managing.Application/Grains/PriceFetcherInitializer.cs @@ -0,0 +1,25 @@ +using Managing.Application.Abstractions.Grains; +using Microsoft.Extensions.Hosting; + +namespace Managing.Application.Grains; + +public class PriceFetcherInitializer : IHostedService +{ + private readonly IClusterClient _clusterClient; + + public PriceFetcherInitializer(IClusterClient clusterClient) + { + _clusterClient = clusterClient; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _clusterClient.GetGrain(0); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Managing.Application/Hubs/CandleHub.cs b/src/Managing.Application/Hubs/CandleHub.cs deleted file mode 100644 index 83ffd3a7..00000000 --- a/src/Managing.Application/Hubs/CandleHub.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Managing.Application.Abstractions.Services; -using Microsoft.AspNetCore.SignalR; - -namespace Managing.Application.Hubs; - -public class CandleHub : Hub -{ - private int ConnectionCount = 0; - private readonly IStreamService _streamService; - - public CandleHub(IStreamService streamService) - { - _streamService = streamService; - } - - public async override Task OnConnectedAsync() - { - ConnectionCount++; - - await Clients.Caller.SendAsync("Message", $"Connected successfully on candle hub. ConnectionId : {Context.ConnectionId}"); - - //await _streamService.SubscribeCandle(async (candle) => { - // await Clients.All.SendAsync("Candle", candle); - //}); - await _streamService.SubscribeCandle(); - await base.OnConnectedAsync(); - - } - - public override async Task OnDisconnectedAsync(Exception ex) - { - await Clients.Caller.SendAsync("Message", $"Shuting down candle hub. ConnectionId : {Context.ConnectionId}"); - - ConnectionCount--; - if(ConnectionCount == 0) - { - await _streamService.UnSubscribeCandle(); - } - await base.OnDisconnectedAsync(ex); - } -} diff --git a/src/Managing.Application/Managing.Application.csproj b/src/Managing.Application/Managing.Application.csproj index b8c73404..09a47e0a 100644 --- a/src/Managing.Application/Managing.Application.csproj +++ b/src/Managing.Application/Managing.Application.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Managing.Application/Shared/StreamService.cs b/src/Managing.Application/Shared/StreamService.cs deleted file mode 100644 index c70f4217..00000000 --- a/src/Managing.Application/Shared/StreamService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Managing.Application.Abstractions.Services; -using Managing.Application.Hubs; -using Microsoft.AspNetCore.SignalR; - -namespace Managing.Application.Shared; - -public class StreamService : IStreamService -{ - private readonly IExchangeStream _exchangeStream; - private readonly IHubContext _hubContext; - - - public StreamService(IExchangeStream exchangeStream, IHubContext hubContext) - { - _exchangeStream = exchangeStream; - _hubContext = hubContext; - } - - public async Task SubscribeCandle() - { - await _exchangeStream.StartBinanceWorker(Common.Enums.Ticker.BTC, async (candle) => { - await _hubContext.Clients.All.SendAsync(candle.Ticker, candle); - }); - } - - public async Task UnSubscribeCandle() - { - await _exchangeStream.StopBinanceWorker(); - } -} diff --git a/src/Managing.Application/Trading/TradingService.cs b/src/Managing.Application/Trading/TradingService.cs index 3037f4e8..62b0a258 100644 --- a/src/Managing.Application/Trading/TradingService.cs +++ b/src/Managing.Application/Trading/TradingService.cs @@ -23,6 +23,7 @@ public class TradingService : ITradingService private readonly ITradingRepository _tradingRepository; private readonly IExchangeService _exchangeService; private readonly IAccountService _accountService; + private readonly IAccountRepository _accountRepository; private readonly ICacheService _cacheService; private readonly IMessengerService _messengerService; private readonly IStatisticRepository _statisticRepository; @@ -35,6 +36,7 @@ public class TradingService : ITradingService IExchangeService exchangeService, ILogger logger, IAccountService accountService, + IAccountRepository accountRepository, ICacheService cacheService, IMessengerService messengerService, IStatisticRepository statisticRepository, @@ -45,6 +47,7 @@ public class TradingService : ITradingService _exchangeService = exchangeService; _logger = logger; _accountService = accountService; + _accountRepository = accountRepository; _cacheService = cacheService; _messengerService = messengerService; _statisticRepository = statisticRepository; @@ -319,7 +322,7 @@ public class TradingService : ITradingService $"[{shortAddress}][{ticker}] No change - Quantity still {newTrade.Quantity}"); } } - catch (Exception ex) + catch (Exception) { _logger.LogError($"[{shortAddress}][{ticker}] Impossible to fetch trader"); } @@ -357,7 +360,7 @@ public class TradingService : ITradingService public List PositionIdentifiers { get; set; } } - public async Task InitPrivyWallet(string publicAddress) + public async Task InitPrivyWallet(string publicAddress, TradingExchanges tradingExchange) { try { @@ -368,7 +371,39 @@ public class TradingService : ITradingService { Success = false, Error = "Public address cannot be null or empty" }; } - return await _evmManager.InitAddress(publicAddress); + // Check if the account is already initialized + var account = await _accountRepository.GetAccountByKeyAsync(publicAddress); + if (account != null && account.IsGmxInitialized) + { + _logger.LogInformation("Account with address {PublicAddress} is already initialized for GMX", publicAddress); + return new PrivyInitAddressResponse + { + Success = true, + Address = publicAddress, + IsAlreadyInitialized = true + }; + } + + PrivyInitAddressResponse initResult; + switch (tradingExchange) + { + case TradingExchanges.GmxV2: + initResult = await _evmManager.InitAddressForGMX(publicAddress); + break; + default: + initResult = await _evmManager.InitAddressForGMX(publicAddress); + break; + } + + // If initialization was successful, update the account's initialization status + if (initResult.Success && account != null) + { + account.IsGmxInitialized = true; + await _accountRepository.UpdateAccountAsync(account); + _logger.LogInformation("Updated account {AccountName} GMX initialization status to true", account.Name); + } + + return initResult; } catch (Exception ex) { diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index de676f90..4463bb33 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -66,9 +66,16 @@ public static class ApiBootstrap .AddWorkers(configuration) .AddFluentValidation() .AddMediatR() + .AddHostedServices() ; } + private static IServiceCollection AddHostedServices(this IServiceCollection services) + { + // services.AddHostedService(); + return services; + } + // Note: IClusterClient is automatically available in co-hosting scenarios // through IGrainFactory. Services should inject IGrainFactory instead of IClusterClient // to avoid circular dependency issues during DI container construction. @@ -90,8 +97,8 @@ public static class ApiBootstrap // Allow disabling Orleans clustering entirely in case of issues var disableOrleansClusteringEnv = Environment.GetEnvironmentVariable("DISABLE_ORLEANS_CLUSTERING"); - var disableOrleansClustering = !string.IsNullOrEmpty(disableOrleansClusteringEnv) && - bool.TryParse(disableOrleansClusteringEnv, out var disabled) && disabled; + var disableOrleansClustering = !string.IsNullOrEmpty(disableOrleansClusteringEnv) && + bool.TryParse(disableOrleansClusteringEnv, out var disabled) && disabled; // Get TASK_SLOT for multi-instance configuration var taskSlotEnv = Environment.GetEnvironmentVariable("TASK_SLOT"); @@ -107,19 +114,19 @@ public static class ApiBootstrap var dashboardPort = 9999 + (taskSlot - 1); // 9999, 10000, 10001, etc. // Get hostname for clustering - prioritize external IP for multi-server setups - var hostname = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ?? // CapRover server IP - Environment.GetEnvironmentVariable("EXTERNAL_IP") ?? // Custom external IP - Environment.GetEnvironmentVariable("HOSTNAME") ?? // Container hostname - Environment.GetEnvironmentVariable("COMPUTERNAME") ?? // Windows hostname - "localhost"; + var hostname = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ?? // CapRover server IP + Environment.GetEnvironmentVariable("EXTERNAL_IP") ?? // Custom external IP + Environment.GetEnvironmentVariable("HOSTNAME") ?? // Container hostname + Environment.GetEnvironmentVariable("COMPUTERNAME") ?? // Windows hostname + "localhost"; // For Docker containers, always use localhost for same-server clustering - IPAddress advertisedIP = IPAddress.Loopback; // Advertise as localhost for same-server clustering - + IPAddress advertisedIP = IPAddress.Loopback; // Advertise as localhost for same-server clustering + // Only use external IP if specifically provided for multi-server scenarios - var externalIP = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ?? - Environment.GetEnvironmentVariable("EXTERNAL_IP"); - + var externalIP = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ?? + Environment.GetEnvironmentVariable("EXTERNAL_IP"); + if (!string.IsNullOrEmpty(externalIP) && IPAddress.TryParse(externalIP, out var parsedExternalIP)) { advertisedIP = parsedExternalIP; @@ -206,7 +213,7 @@ public static class ApiBootstrap options.GatewayListeningEndpoint = new IPEndPoint(IPAddress.Any, gatewayPort); }); } - + siloBuilder .Configure(options => { @@ -221,7 +228,7 @@ public static class ApiBootstrap options.ProbeTimeout = TimeSpan.FromSeconds(10); options.IAmAliveTablePublishTimeout = TimeSpan.FromSeconds(30); options.MaxJoinAttemptTime = TimeSpan.FromSeconds(120); - + // Improved settings for development environments with stale members options.DefunctSiloCleanupPeriod = TimeSpan.FromMinutes(1); options.DefunctSiloExpiration = TimeSpan.FromMinutes(2); @@ -292,6 +299,11 @@ public static class ApiBootstrap options.Invariant = "Npgsql"; }) .AddAdoNetGrainStorage("platform-summary-store", options => + { + options.ConnectionString = postgreSqlConnectionString; + options.Invariant = "Npgsql"; + }) + .AddAdoNetGrainStorage("candle-store", options => { options.ConnectionString = postgreSqlConnectionString; options.Invariant = "Npgsql"; @@ -304,9 +316,14 @@ public static class ApiBootstrap .AddMemoryGrainStorage("bot-store") .AddMemoryGrainStorage("registry-store") .AddMemoryGrainStorage("agent-store") - .AddMemoryGrainStorage("platform-summary-store"); + .AddMemoryGrainStorage("platform-summary-store") + .AddMemoryGrainStorage("candle-store"); } + // Configure Orleans Streams for price data distribution + siloBuilder.AddMemoryStreams("DefaultStreamProvider") + .AddMemoryGrainStorage("PubSubStore"); + siloBuilder .ConfigureServices(services => { @@ -316,6 +333,7 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); }); }) ; @@ -347,7 +365,6 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); @@ -357,7 +374,7 @@ public static class ApiBootstrap services.AddSingleton(); services.AddSingleton(); - + // Admin services services.AddSingleton(); diff --git a/src/Managing.Bootstrap/Managing.Bootstrap.csproj b/src/Managing.Bootstrap/Managing.Bootstrap.csproj index 8e790465..788b87f2 100644 --- a/src/Managing.Bootstrap/Managing.Bootstrap.csproj +++ b/src/Managing.Bootstrap/Managing.Bootstrap.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Managing.Common/Constants.cs b/src/Managing.Common/Constants.cs index 12ec19b1..7029ac83 100644 --- a/src/Managing.Common/Constants.cs +++ b/src/Managing.Common/Constants.cs @@ -1,4 +1,6 @@ -namespace Managing.Common +using static Managing.Common.Enums; + +namespace Managing.Common { public class Constants { @@ -65,21 +67,41 @@ { public const string OracleKeeperUrl = "https://arbitrum-v2-1-api.gmxinfra.io"; - public static readonly HashSet DeltaNeutralTickers = new() + public static readonly HashSet DeltaNeutralTickers = new() { - Enums.Ticker.BTC, - Enums.Ticker.ARB, - Enums.Ticker.ETH, - Enums.Ticker.BNB, - Enums.Ticker.SOL, - Enums.Ticker.LINK, - Enums.Ticker.OP, - Enums.Ticker.UNI, - Enums.Ticker.AAVE, - Enums.Ticker.PEPE, - Enums.Ticker.WIF, + Ticker.BTC, + Ticker.ARB, + Ticker.ETH, + Ticker.BNB, + Ticker.SOL, + Ticker.LINK, + Ticker.OP, + Ticker.UNI, + Ticker.AAVE, + Ticker.PEPE, + Ticker.WIF, }; + public static readonly Ticker[] SupportedTickers = + { + Ticker.BTC, + Ticker.ETH, + Ticker.BNB, + Ticker.DOGE, + Ticker.ADA, + Ticker.SOL, + Ticker.XRP, + Ticker.LINK, + Ticker.RENDER, + Ticker.SUI, + Ticker.GMX, + Ticker.ARB, + Ticker.PEPE, + Ticker.PENDLE, + Ticker.AAVE, + Ticker.HYPE + }; + public static class Decimals { public const int USD = 30; diff --git a/src/Managing.Domain/Accounts/Account.cs b/src/Managing.Domain/Accounts/Account.cs index fe467a6e..fead50f2 100644 --- a/src/Managing.Domain/Accounts/Account.cs +++ b/src/Managing.Domain/Accounts/Account.cs @@ -28,6 +28,9 @@ public class Account [Id(6)] public List Balances { get; set; } + + [Id(7)] + public bool IsGmxInitialized { get; set; } = false; public bool IsPrivyWallet => Type == AccountType.Privy; } \ No newline at end of file diff --git a/src/Managing.Domain/Accounts/ExchangeApprovalStatus.cs b/src/Managing.Domain/Accounts/ExchangeApprovalStatus.cs new file mode 100644 index 00000000..1f9dbb98 --- /dev/null +++ b/src/Managing.Domain/Accounts/ExchangeApprovalStatus.cs @@ -0,0 +1,14 @@ +using Orleans; +using static Managing.Common.Enums; + +namespace Managing.Domain.Accounts; + +[GenerateSerializer] +public class ExchangeApprovalStatus +{ + [Id(0)] + public TradingExchanges Exchange { get; set; } + + [Id(1)] + public bool IsApproved { get; set; } +} diff --git a/src/Managing.Domain/Candles/Candle.cs b/src/Managing.Domain/Candles/Candle.cs index d316704c..a17ee7ac 100644 --- a/src/Managing.Domain/Candles/Candle.cs +++ b/src/Managing.Domain/Candles/Candle.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; -using Managing.Common; using Orleans; using Skender.Stock.Indicators; +using static Managing.Common.Enums; namespace Managing.Domain.Candles { @@ -9,10 +9,10 @@ namespace Managing.Domain.Candles public class Candle : IQuote { [Id(0)] - [Required] public Enums.TradingExchanges Exchange { get; set; } + [Required] public TradingExchanges Exchange { get; set; } [Id(1)] - [Required] public string Ticker { get; set; } + [Required] public Ticker Ticker { get; set; } [Id(2)] [Required] public DateTime OpenTime { get; set; } @@ -33,7 +33,7 @@ namespace Managing.Domain.Candles [Required] public decimal Low { get; set; } [Id(8)] - [Required] public Enums.Timeframe Timeframe { get; set; } + [Required] public Timeframe Timeframe { get; set; } [Id(9)] public decimal Volume { get; set; } diff --git a/src/Managing.Domain/Evm/PrivyInitAddressResponse.cs b/src/Managing.Domain/Evm/PrivyInitAddressResponse.cs index 20b5e91c..7d426275 100644 --- a/src/Managing.Domain/Evm/PrivyInitAddressResponse.cs +++ b/src/Managing.Domain/Evm/PrivyInitAddressResponse.cs @@ -8,4 +8,6 @@ public class PrivyInitAddressResponse public string? OrderVaultHash { get; set; } public string? ExchangeRouterHash { get; set; } public string? Error { get; set; } + public string? Address { get; set; } + public bool IsAlreadyInitialized { get; set; } } \ No newline at end of file diff --git a/src/Managing.Domain/Indicators/Context/StDevContext.cs b/src/Managing.Domain/Indicators/Context/StDevContext.cs index 4b1ee0d3..74d1b418 100644 --- a/src/Managing.Domain/Indicators/Context/StDevContext.cs +++ b/src/Managing.Domain/Indicators/Context/StDevContext.cs @@ -114,7 +114,7 @@ public class StDevContext : IndicatorBase Confidence confidence) { var signal = new LightSignal( - MiscExtensions.ParseEnum(candleSignal.Ticker), + candleSignal.Ticker, direction, confidence, candleSignal, diff --git a/src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs index d21adb8c..a0874f5d 100644 --- a/src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs @@ -108,7 +108,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase Confidence confidence) { var signal = new LightSignal( - MiscExtensions.ParseEnum(candleSignal.Ticker), + candleSignal.Ticker, direction, confidence, candleSignal, diff --git a/src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs index 4af58bf0..809d74b7 100644 --- a/src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs @@ -105,7 +105,7 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase private void AddSignal(CandleDualEma candleSignal, TradeDirection direction, Confidence confidence) { - var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, + var signal = new LightSignal(candleSignal.Ticker, direction, confidence, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { diff --git a/src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs index 581e8adf..60f5d744 100644 --- a/src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs @@ -69,7 +69,7 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence) { - var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, + var signal = new LightSignal(candleSignal.Ticker, direction, confidence, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { diff --git a/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs index 327317b6..fb2c2857 100644 --- a/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs @@ -69,7 +69,7 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence) { - var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, + var signal = new LightSignal(candleSignal.Ticker, direction, confidence, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { diff --git a/src/Managing.Domain/Indicators/Signals/LaggingSTC.cs b/src/Managing.Domain/Indicators/Signals/LaggingSTC.cs index 6d58c2fa..76ae3640 100644 --- a/src/Managing.Domain/Indicators/Signals/LaggingSTC.cs +++ b/src/Managing.Domain/Indicators/Signals/LaggingSTC.cs @@ -125,7 +125,7 @@ public class LaggingSTC : IndicatorBase private void AddSignal(CandleSct candleSignal, TradeDirection direction, Confidence confidence) { var signal = new LightSignal( - MiscExtensions.ParseEnum(candleSignal.Ticker), + candleSignal.Ticker, direction, confidence, candleSignal, diff --git a/src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs index b54a8480..5fcdc980 100644 --- a/src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs @@ -105,7 +105,7 @@ public class MacdCrossIndicatorBase : IndicatorBase private void AddSignal(CandleMacd candleSignal, TradeDirection direction, Confidence confidence) { - var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, + var signal = new LightSignal(candleSignal.Ticker, direction, confidence, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { diff --git a/src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs index b080b978..791447ab 100644 --- a/src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs @@ -233,7 +233,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase private void AddSignal(CandleRsi candleSignal, TradeDirection direction, Confidence confidence) { - var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, + var signal = new LightSignal(candleSignal.Ticker, direction, confidence, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { diff --git a/src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs index e1a27be8..641d29af 100644 --- a/src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs @@ -206,7 +206,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase private void AddSignal(CandleRsi candleSignal, TradeDirection direction, HashSet candles) { - var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, Confidence.Low, + var signal = new LightSignal(candleSignal.Ticker, direction, Confidence.Low, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (Signals.Count(s => s.Identifier == signal.Identifier) < 1) diff --git a/src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs index a2ee4943..f7cf85f9 100644 --- a/src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs @@ -106,7 +106,7 @@ public class StcIndicatorBase : IndicatorBase private void AddSignal(CandleSct candleSignal, TradeDirection direction, Confidence confidence) { var signal = new LightSignal( - MiscExtensions.ParseEnum(candleSignal.Ticker), + candleSignal.Ticker, direction, confidence, candleSignal, diff --git a/src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs b/src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs index e60444db..03ada7c3 100644 --- a/src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs +++ b/src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs @@ -170,7 +170,7 @@ public class SuperTrendCrossEma : IndicatorBase private void AddSignal(CandleSuperTrend candleSignal, TradeDirection direction, Confidence confidence) { - var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, + var signal = new LightSignal(candleSignal.Ticker, direction, confidence, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) diff --git a/src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs index 06f3dc08..bf17e4b5 100644 --- a/src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs @@ -107,7 +107,7 @@ public class SuperTrendIndicatorBase : IndicatorBase private void AddSignal(CandleSuperTrend candleSignal, TradeDirection direction, Confidence confidence) { - var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, + var signal = new LightSignal(candleSignal.Ticker, direction, confidence, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) diff --git a/src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs b/src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs index db870156..4725b13b 100644 --- a/src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs @@ -66,7 +66,7 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase public void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence) { - var signal = new LightSignal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, + var signal = new LightSignal(candleSignal.Ticker, direction, confidence, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { diff --git a/src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs b/src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs index b8ff7ec0..b7236aec 100644 --- a/src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs @@ -102,7 +102,7 @@ public class StochRsiTrendIndicatorBase : IndicatorBase private void AddSignal(CandleStochRsi candleSignal, TradeDirection direction, Confidence confidence) { var signal = new LightSignal( - MiscExtensions.ParseEnum(candleSignal.Ticker), + candleSignal.Ticker, direction, confidence, candleSignal, diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index 424d288f..ae8a67b1 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -1,5 +1,4 @@ -using Managing.Core; -using Managing.Domain.Candles; +using Managing.Domain.Candles; using Managing.Domain.Indicators; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; @@ -124,7 +123,7 @@ public static class TradingBox } var data = newCandles.First(); - return ComputeSignals(lightScenario, latestSignalsPerIndicator, MiscExtensions.ParseEnum(data.Ticker), + return ComputeSignals(lightScenario, latestSignalsPerIndicator, data.Ticker, data.Timeframe, config); } diff --git a/src/Managing.Infrastructure.Database/InfluxDb/PriceHelpers.cs b/src/Managing.Infrastructure.Database/InfluxDb/PriceHelpers.cs index 3fa8e61a..0b7d49dd 100644 --- a/src/Managing.Infrastructure.Database/InfluxDb/PriceHelpers.cs +++ b/src/Managing.Infrastructure.Database/InfluxDb/PriceHelpers.cs @@ -12,7 +12,7 @@ public static class PriceHelpers var price = new PriceDto { Exchange = candle.Exchange.ToString(), - Ticker = candle.Ticker, + Ticker = candle.Ticker.ToString(), OpenTime = candle.OpenTime, Open = candle.Open, Close = candle.Close, @@ -30,7 +30,7 @@ public static class PriceHelpers return new Candle { Exchange = MiscExtensions.ParseEnum(dto.Exchange), - Ticker = dto.Ticker, + Ticker = MiscExtensions.ParseEnum(dto.Ticker), OpenTime = dto.OpenTime, Open = dto.Open, Close = dto.Close, diff --git a/src/Managing.Infrastructure.Database/Migrations/20250912190732_AddIsGmxInitializedToAccount.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250912190732_AddIsGmxInitializedToAccount.Designer.cs new file mode 100644 index 00000000..3fa17738 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250912190732_AddIsGmxInitializedToAccount.Designer.cs @@ -0,0 +1,1440 @@ +// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20250912190732_AddIsGmxInitializedToAccount")] + partial class AddIsGmxInitializedToAccount + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGmxInitialized") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalBalance") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("UserId"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserId", "Score"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("LongPositionCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("ShortPositionCount") + .HasColumnType("integer"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BacktestRequestsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "CreatedAt"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitiatorIdentifier") + .HasColumnType("uuid"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("InitiatorIdentifier"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Date"); + + b.HasIndex("Identifier", "Date", "UserId") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Fee") + .HasColumnType("decimal(18,8)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250912190732_AddIsGmxInitializedToAccount.cs b/src/Managing.Infrastructure.Database/Migrations/20250912190732_AddIsGmxInitializedToAccount.cs new file mode 100644 index 00000000..69d2f162 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250912190732_AddIsGmxInitializedToAccount.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class AddIsGmxInitializedToAccount : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsGmxInitialized", + table: "Accounts", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsGmxInitialized", + table: "Accounts"); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs index e7e12029..d726db58 100644 --- a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs +++ b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs @@ -34,6 +34,11 @@ namespace Managing.Infrastructure.Databases.Migrations .IsRequired() .HasColumnType("text"); + b.Property("IsGmxInitialized") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("Key") .IsRequired() .HasMaxLength(500) diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/AccountEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/AccountEntity.cs index aac1ab11..8d8a4020 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/AccountEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/AccountEntity.cs @@ -8,12 +8,13 @@ namespace Managing.Infrastructure.Databases.PostgreSql.Entities; public class AccountEntity { [Key] public int Id { get; set; } - [Required] public string Name { get; set; } + [Required] public required string Name { get; set; } [Required] public TradingExchanges Exchange { get; set; } [Required] public AccountType Type { get; set; } [Required] public string? Key { get; set; } public string? Secret { get; set; } [Required] public int UserId { get; set; } + [Required] public bool IsGmxInitialized { get; set; } = false; // Navigation properties public UserEntity? User { get; set; } diff --git a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs index 360041e9..455e6334 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs @@ -64,6 +64,9 @@ public class ManagingDbContext : DbContext entity.Property(e => e.Type) .IsRequired() .HasConversion(); // 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(); diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlAccountRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlAccountRepository.cs index 8d998cc8..f8d67c5b 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlAccountRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlAccountRepository.cs @@ -72,7 +72,7 @@ public class PostgreSqlAccountRepository : IAccountRepository _cacheService.SaveValue(cacheKey, account, TimeSpan.FromHours(1)); return account; } - catch (Exception ex) + catch (Exception) { // If there's an error, try to reset the connection throw; @@ -117,4 +117,34 @@ public class PostgreSqlAccountRepository : IAccountRepository _context.Accounts.Add(accountEntity); await _context.SaveChangesAsync().ConfigureAwait(false); } + + public async Task UpdateAccountAsync(Account account) + { + try + { + await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); + + var existingEntity = await _context.Accounts + .AsTracking() + .FirstOrDefaultAsync(a => a.Name == account.Name) + .ConfigureAwait(false); + + if (existingEntity == null) + { + throw new ArgumentException($"Account '{account.Name}' not found"); + } + + // Update properties + existingEntity.IsGmxInitialized = account.IsGmxInitialized; + await _context.SaveChangesAsync().ConfigureAwait(false); + + // Clear cache for this account + var cacheKey = $"account_{account.Name}"; + _cacheService.RemoveValue(cacheKey); + } + finally + { + await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); + } + } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs index 22062d7d..b39ae485 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs @@ -34,7 +34,8 @@ public static class PostgreSqlMappers Key = entity.Key, Secret = entity.Secret, User = entity.User != null ? Map(entity.User) : null, - Balances = new List() // Empty list for now, balances handled separately if needed + Balances = new List(), // Empty list for now, balances handled separately if needed + IsGmxInitialized = entity.IsGmxInitialized }; } @@ -50,6 +51,7 @@ public static class PostgreSqlMappers Key = account.Key, Secret = account.Secret, UserId = account.User.Id, + IsGmxInitialized = account.IsGmxInitialized }; } diff --git a/src/Managing.Infrastructure.Exchanges/CandleHelpers.cs b/src/Managing.Infrastructure.Exchanges/CandleHelpers.cs index 3498d4da..c165199a 100644 --- a/src/Managing.Infrastructure.Exchanges/CandleHelpers.cs +++ b/src/Managing.Infrastructure.Exchanges/CandleHelpers.cs @@ -23,7 +23,7 @@ namespace Managing.Infrastructure.Exchanges { return new Candle() { - Ticker = ticker.ToString(), + Ticker = ticker, Timeframe = timeframe, Volume = candle.Volume, Close = candle.ClosePrice, diff --git a/src/Managing.Infrastructure.Exchanges/ExchangeStream.cs b/src/Managing.Infrastructure.Exchanges/ExchangeStream.cs deleted file mode 100644 index eaf06909..00000000 --- a/src/Managing.Infrastructure.Exchanges/ExchangeStream.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Binance.Net.Interfaces.Clients; -using Managing.Application.Abstractions.Services; -using Managing.Application.Shared; -using Managing.Domain.Candles; -using Managing.Infrastructure.Exchanges.Helpers; -using Microsoft.Extensions.Logging; -using static Managing.Common.Enums; - -namespace Managing.Infrastructure.Exchanges; - -public class ExchangeStream : IExchangeStream -{ - private readonly ILogger logger; - private readonly IBinanceSocketClient _binanceSocketClient; - - public ExchangeStream(IBinanceSocketClient binanceSocketClient, ILogger logger) - { - _binanceSocketClient = binanceSocketClient; - this.logger = logger; - } - - public async Task StartBinanceWorker(Ticker ticker, Func action) - { - logger.LogInformation($"Starting binance worker for {ticker}"); - - await _binanceSocketClient.SpotApi.ExchangeData.SubscribeToKlineUpdatesAsync(BinanceHelpers.ToBinanceTicker(ticker), Binance.Net.Enums.KlineInterval.OneSecond, candle => - { - if (candle.Data.Data?.Final == true) - { - //action((candle) => { CandleHelpers.Map(candle.Data.Data, ticker, Timeframe.FiveMinutes)}); - action(CandleHelpers.Map(candle.Data.Data, ticker, Timeframe.FiveMinutes)); - } - }); - } - - public async Task StopBinanceWorker() - { - logger.LogInformation($"Stoping all Binance worker subscription"); - await _binanceSocketClient.UnsubscribeAllAsync(); - } -} diff --git a/src/Managing.Infrastructure.Exchanges/Helpers/BinanceHelpers.cs b/src/Managing.Infrastructure.Exchanges/Helpers/BinanceHelpers.cs deleted file mode 100644 index 48717389..00000000 --- a/src/Managing.Infrastructure.Exchanges/Helpers/BinanceHelpers.cs +++ /dev/null @@ -1,186 +0,0 @@ -using Binance.Net.Enums; -using Binance.Net.Interfaces; -using Binance.Net.Objects.Models.Futures; -using CryptoExchange.Net.Objects; -using Managing.Core; -using Managing.Domain.Candles; -using Managing.Domain.Trades; -using static Managing.Common.Enums; - -namespace Managing.Infrastructure.Exchanges.Helpers -{ - public static class BinanceHelpers - { - public static Trade Map(BinanceFuturesOrder data) - { - if (data == null) - return null; - - return new Trade(data.CreateTime, - (data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short, - (TradeStatus)data.Status, (TradeType)data.OriginalType, MiscExtensions.ParseEnum(data.Symbol), - data.Quantity, data.AveragePrice, 1, - data.ClientOrderId, ""); - } - - public static FuturesOrderType BinanceOrderTypeMap(TradeType tradeType) - { - switch (tradeType) - { - case TradeType.Limit: - return FuturesOrderType.Limit; - case TradeType.Market: - return FuturesOrderType.Market; - case TradeType.StopMarket: - return FuturesOrderType.StopMarket; - case TradeType.StopLossLimit: - return FuturesOrderType.Stop; - default: - return FuturesOrderType.Limit; - } - } - - public static TradeType BinanceOrderTradeType(FuturesOrderType orderType) - { - switch (orderType) - { - case FuturesOrderType.Stop: - case FuturesOrderType.Limit: - return TradeType.Limit; - case FuturesOrderType.Market: - return TradeType.Market; - case FuturesOrderType.StopMarket: - return TradeType.StopMarket; - default: - return TradeType.Limit; - } - } - - public static Trade Map(WebCallResult result, decimal? leverage = null) - { - var data = result.Data; - - if (data == null) - { - return new Trade(DateTime.Now, TradeDirection.None, - TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0, - "", result.Error?.Message); - } - - return new Trade(DateTime.Now, TradeDirection.None, - TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0, - "", result.Error?.Message); - } - - public static Candle Map(IBinanceKline binanceKline, Ticker ticker, TradingExchanges exchange) - { - return new Candle - { - Date = binanceKline.CloseTime, - Volume = binanceKline.Volume, - Close = binanceKline.ClosePrice, - High = binanceKline.HighPrice, - Low = binanceKline.LowPrice, - Open = binanceKline.OpenPrice, - Ticker = ticker.ToString(), - OpenTime = binanceKline.OpenTime, - Exchange = exchange - }; - } - - internal static KlineInterval Map(Timeframe interval) => interval switch - { - Timeframe.FiveMinutes => KlineInterval.FiveMinutes, - Timeframe.FifteenMinutes => KlineInterval.FifteenMinutes, - Timeframe.ThirtyMinutes => KlineInterval.ThirtyMinutes, - Timeframe.OneHour => KlineInterval.OneHour, - Timeframe.FourHour => KlineInterval.FourHour, - Timeframe.OneDay => KlineInterval.OneDay, - _ => throw new NotImplementedException(), - }; - - public static string ToBinanceTicker(Ticker ticker) - { - switch (ticker) - { - case Ticker.ADA: - return "ADAUSDT"; - case Ticker.ALGO: - return "ALGOUSDT"; - case Ticker.ATOM: - return "ATOMUSDT"; - case Ticker.AVAX: - return "AVAXUSDT"; - case Ticker.BNB: - return "BNBUSDT"; - case Ticker.BTC: - return "BTCUSDT"; - case Ticker.CRV: - return "CRVUSDT"; - case Ticker.DOGE: - return "DOGEUSDT"; - case Ticker.DOT: - return "DOTUSDT"; - case Ticker.DYDX: - return "DYDXUSDT"; - case Ticker.ETC: - return "ETCUSDT"; - case Ticker.ETH: - return "ETHUSDT"; - case Ticker.FTM: - return "FTMUSDT"; - case Ticker.GALA: - return "GALAUSDT"; - case Ticker.GRT: - return "GRTUSDT"; - case Ticker.IMX: - return "IMXUSDT"; - case Ticker.KSM: - return "KSMUSDT"; - case Ticker.LINK: - return "LINKUSDT"; - case Ticker.LRC: - return "LRCUSDT"; - case Ticker.LTC: - return "LTCUSDT"; - case Ticker.MATIC: - return "MATICUSDT"; - case Ticker.MKR: - return "MKRUSDT"; - case Ticker.NEAR: - return "NEARUSDT"; - case Ticker.SAND: - return "SANDUSDT"; - case Ticker.SOL: - return "SOLUSDT"; - case Ticker.SRM: - return "SRMUSDT"; - case Ticker.SUSHI: - return "SUSHIUSDT"; - case Ticker.THETA: - return "THETAUSDT"; - case Ticker.UNI: - return "UNIUSDT"; - case Ticker.XMR: - return "XMRUSDT"; - case Ticker.XRP: - return "XRPUSDT"; - case Ticker.XTZ: - return "XTZUSDT"; - default: - break; - } - - throw new NotImplementedException(); - } - - internal static object Map(WebCallResult binanceResult, decimal? leverage) - { - throw new NotImplementedException(); - } - } - - public class BinanceFuturesPlacedOrder - { - } -} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Exchanges/Helpers/FtxHelpers.cs b/src/Managing.Infrastructure.Exchanges/Helpers/FtxHelpers.cs deleted file mode 100644 index ac4dc67a..00000000 --- a/src/Managing.Infrastructure.Exchanges/Helpers/FtxHelpers.cs +++ /dev/null @@ -1,239 +0,0 @@ -using CryptoExchange.Net.Objects; -using FTX.Net.Enums; -using FTX.Net.Objects.Models; -using Managing.Core; -using Managing.Domain.Candles; -using Managing.Domain.Trades; -using static Managing.Common.Enums; - -namespace Managing.Infrastructure.Exchanges.Helpers -{ - public static class FtxHelpers - { - public static string ToFtxTicker(Ticker ticker) - { - switch (ticker) - { - case Ticker.ADA: - return "ADA-PERP"; - case Ticker.APE: - return "APE-PERP"; - case Ticker.ALGO: - return "ALGO-PERP"; - case Ticker.ATOM: - return "ATOM-PERP"; - case Ticker.AVAX: - return "AVAX-PERP"; - case Ticker.BNB: - return "BNB-PERP"; - case Ticker.BTC: - return "BTC-PERP"; - case Ticker.BAL: - return "BAL-PERP"; - case Ticker.CHZ: - return "CHZ-PERP"; - case Ticker.COMP: - return "COMP-PERP"; - case Ticker.CRO: - return "CRO-PERP"; - case Ticker.CRV: - return "CRV-PERP"; - case Ticker.DOGE: - return "DOGE-PERP"; - case Ticker.DOT: - return "DOT-PERP"; - case Ticker.DYDX: - return "DYDX-PERP"; - case Ticker.ENS: - return "ENS-PERP"; - case Ticker.ETC: - return "ETC-PERP"; - case Ticker.ETH: - return "ETH-PERP"; - case Ticker.FIL: - return "FIL-PERP"; - case Ticker.FLM: - return "FLM-PERP"; - case Ticker.FTM: - return "FTM-PERP"; - case Ticker.GALA: - return "GALA-PERP"; - case Ticker.GRT: - return "GRT-PERP"; - case Ticker.KSM: - return "KSM-PERP"; - case Ticker.LDO: - return "LDO-PERP"; - case Ticker.LINK: - return "LINK-PERP"; - case Ticker.LRC: - return "LRC-PERP"; - case Ticker.LTC: - return "LTC-PERP"; - case Ticker.MANA: - return "MANA-PERP"; - case Ticker.MATIC: - return "MATIC-PERP"; - case Ticker.MKR: - return "MKR-PERP"; - case Ticker.NEAR: - return "NEAR-PERP"; - case Ticker.QTUM: - return "QTUM-PERP"; - case Ticker.REN: - return "REN-PERP"; - case Ticker.ROSE: - return "ROSE-PERP"; - case Ticker.RSR: - return "RSR-PERP"; - case Ticker.RUNE: - return "RUNE-PERP"; - case Ticker.SAND: - return "SAND-PERP"; - case Ticker.SOL: - return "SOL-PERP"; - case Ticker.SRM: - return "SRM-PERP"; - case Ticker.SUSHI: - return "SUSHI-PERP"; - case Ticker.THETA: - return "THETA-PERP"; - case Ticker.UNI: - return "UNI-PERP"; - case Ticker.XMR: - return "XMR-PERP"; - case Ticker.XRP: - return "XRP-PERP"; - case Ticker.XTZ: - return "XTZ-PERP"; - default: - break; - } - - throw new NotImplementedException(); - } - - - public static Trade Map(WebCallResult result, decimal? leverage = null) - { - var data = result.Data; - - if (data == null) - { - return new Trade(DateTime.Now, TradeDirection.None, - TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0, - "", result.Error?.Message); - } - - return new Trade(data.CreateTime, - (data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short, - (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum(data.Symbol), - data.Quantity, data.AverageFillPrice ?? 0, leverage, - data.ClientOrderId, ""); - } - - internal static Trade Map(WebCallResult ftxResult, decimal? leverage) - { - var data = ftxResult.Data; - - if (data == null) - { - return new Trade(DateTime.Now, TradeDirection.None, - TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0, - "", ftxResult.Error?.Message); - } - - return new Trade(data.CreateTime, - (data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short, - (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum(data.Symbol), - data.Quantity, data.TriggerPrice ?? 0, leverage, - Guid.NewGuid().ToString(), ""); - } - - public static OrderType FtxOrderTypeMap(TradeType tradeType) - { - switch (tradeType) - { - case TradeType.Limit: - return OrderType.Limit; - case TradeType.Market: - return OrderType.Market; - default: - return OrderType.Limit; - } - } - - public static Trade Map(FTXOrder data) - { - if (data == null) - return null; - - return new Trade(data.CreateTime, TradeDirection.None, - (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum(data.Symbol), - data.Quantity, data.AverageFillPrice ?? 0, 0, - data.ClientOrderId, ""); - } - - public static Candle Map( - FTXKline ftxKline, - Ticker ticker, - TradingExchanges exchange, - Timeframe timeframe) - { - return new Candle - { - Date = ftxKline.OpenTime, - Volume = ftxKline.Volume ?? 0, - Close = ftxKline.ClosePrice, - High = ftxKline.HighPrice, - Low = ftxKline.LowPrice, - Open = ftxKline.OpenPrice, - Ticker = ticker.ToString(), - OpenTime = ftxKline.OpenTime, - Exchange = exchange, - Timeframe = timeframe - }; - } - - internal static KlineInterval Map(Timeframe interval) => interval switch - { - Timeframe.FiveMinutes => KlineInterval.FiveMinutes, - Timeframe.FifteenMinutes => KlineInterval.FifteenMinutes, - Timeframe.OneHour => KlineInterval.OneHour, - Timeframe.FourHour => KlineInterval.FourHours, - Timeframe.OneDay => KlineInterval.OneDay, - _ => throw new NotImplementedException(), - }; - - internal static TriggerOrderType FtxTriggerOrderTypeMap(TradeType tradeType) => tradeType switch - { - TradeType.StopMarket => TriggerOrderType.Stop, - TradeType.StopLimit => TriggerOrderType.Stop, - TradeType.StopLoss => TriggerOrderType.Stop, - TradeType.TakeProfit => TriggerOrderType.TakeProfit, - TradeType.StopLossProfit => TriggerOrderType.Stop, - TradeType.StopLossProfitLimit => TriggerOrderType.Stop, - TradeType.StopLossLimit => TriggerOrderType.Stop, - TradeType.TakeProfitLimit => TriggerOrderType.TakeProfit, - TradeType.TrailingStop => TriggerOrderType.TrailingStop, - TradeType.TrailingStopLimit => TriggerOrderType.TrailingStop, - TradeType.StopLossAndLimit => TriggerOrderType.Stop, - TradeType.SettlePosition => TriggerOrderType.Stop, - _ => throw new NotImplementedException(), - }; - - internal static Orderbook Map(WebCallResult ftxOrderBook) - { - return new Orderbook() - { - Asks = Map(ftxOrderBook.Data.Asks), - Bids = Map(ftxOrderBook.Data.Bids) - }; - } - - private static List Map(IEnumerable entry) - { - return entry.Select(ask => new OrderBookEntry() { Price = ask.Price, Quantity = ask.Quantity }).ToList(); - } - } -} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Exchanges/Helpers/KrakenHelpers.cs b/src/Managing.Infrastructure.Exchanges/Helpers/KrakenHelpers.cs deleted file mode 100644 index 3abbb2eb..00000000 --- a/src/Managing.Infrastructure.Exchanges/Helpers/KrakenHelpers.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Kraken.Net.Objects.Models; -using Managing.Core; -using Managing.Domain.Trades; -using static Managing.Common.Enums; - -namespace Managing.Infrastructure.Exchanges.Helpers; - -public static class KrakenHelpers -{ - public static Trade Map(KrakenOrder order) - { - var leverageParse = order.OrderDetails.Leverage.Split((char)':')[0]; - long.TryParse(leverageParse, out long leverage); - - return new Trade(order.CreateTime, - TradeDirection.None, - (TradeStatus)order.Status, - (TradeType)order.OrderDetails.Type, - MiscExtensions.ParseEnum(order.OrderDetails.Symbol), - order.Quantity, - order.AveragePrice, - leverage, - order.ClientOrderId, - ""); - } - - public static Kraken.Net.Enums.OrderType KrakenOrderTypeMap(TradeType tradeType) - { - switch (tradeType) - { - case TradeType.Limit: - return Kraken.Net.Enums.OrderType.Limit; - case TradeType.Market: - return Kraken.Net.Enums.OrderType.Market; - case TradeType.StopMarket: - return Kraken.Net.Enums.OrderType.StopMarket; - default: - return Kraken.Net.Enums.OrderType.Limit; - } - } - - internal static Trade Map(KeyValuePair o) - { - throw new NotImplementedException(); - } -} diff --git a/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj b/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj index f3d153eb..be0e650d 100644 --- a/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj +++ b/src/Managing.Infrastructure.Exchanges/Managing.Infrastructure.Exchanges.csproj @@ -1,23 +1,27 @@ - - net8.0 - enable - AnyCPU;x64 - + + net8.0 + enable + AnyCPU;x64 + - - - - - - - - + + + + + + + + - - - - + + + + + + + + diff --git a/src/Managing.Infrastructure.Tests/EvmManagerTests.cs b/src/Managing.Infrastructure.Tests/EvmManagerTests.cs index faa76335..a255f513 100644 --- a/src/Managing.Infrastructure.Tests/EvmManagerTests.cs +++ b/src/Managing.Infrastructure.Tests/EvmManagerTests.cs @@ -208,7 +208,7 @@ public class EvmManagerTests [Fact] public async Task Should_Init_Address_For_Trading() { - var accountInitilized = await _manager.InitAddress(PublicAddress); + var accountInitilized = await _manager.InitAddressForGMX(PublicAddress); Assert.NotNull(accountInitilized); } diff --git a/src/Managing.Infrastructure.Web3/EvmManager.cs b/src/Managing.Infrastructure.Web3/EvmManager.cs index 65dbe9d6..e5f7629c 100644 --- a/src/Managing.Infrastructure.Web3/EvmManager.cs +++ b/src/Managing.Infrastructure.Web3/EvmManager.cs @@ -506,7 +506,7 @@ public class EvmManager : IEvmManager return lastCandles.Last(); } - public async Task InitAddress(string publicAddress) + public async Task InitAddressForGMX(string publicAddress) { try { diff --git a/src/Managing.Infrastructure.Web3/Extensions/PriceExtensions.cs b/src/Managing.Infrastructure.Web3/Extensions/PriceExtensions.cs index 79b312c6..8fd65750 100644 --- a/src/Managing.Infrastructure.Web3/Extensions/PriceExtensions.cs +++ b/src/Managing.Infrastructure.Web3/Extensions/PriceExtensions.cs @@ -9,7 +9,7 @@ public static class PriceExtensions { public static List GetCandles(this Round[] prices, Timeframe timeframe, Ticker ticker) { - int timezoneOffset = - (int)(new DateTimeOffset(DateTime.UtcNow).Offset.TotalSeconds); + int timezoneOffset = -(int)(new DateTimeOffset(DateTime.UtcNow).Offset.TotalSeconds); var CHART_PERIODS = new Dictionary { { Timeframe.FiveMinutes, 60 * 5 }, @@ -53,6 +53,7 @@ public static class PriceExtensions h = Math.Max(o, c); l = Math.Min(o, c); } + c = decimal.Parse(current.Value); h = Math.Max(h, c); l = Math.Min(l, c); @@ -69,7 +70,7 @@ public static class PriceExtensions Low = x.Low, Timeframe = x.Timeframe, Exchange = TradingExchanges.Evm, - Ticker = ticker.ToString() + Ticker = ticker }).ToList(); } -} +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxMappers.cs b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxMappers.cs index 4bcafed8..fa7a20ce 100644 --- a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxMappers.cs +++ b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxMappers.cs @@ -174,7 +174,7 @@ public static class GmxMappers Low = Convert.ToDecimal(price.l), Close = Convert.ToDecimal(price.c), Exchange = TradingExchanges.Evm, - Ticker = ticker.ToString(), + Ticker = ticker, Timeframe = timeframe }; } diff --git a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs index 8b77930d..14ca91b4 100644 --- a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs +++ b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs @@ -122,7 +122,7 @@ internal static class GmxV2Mappers Low = Convert.ToDecimal(marketPrices[3]), Close = Convert.ToDecimal(marketPrices[4]), Exchange = TradingExchanges.Evm, - Ticker = ticker.ToString(), + Ticker = ticker, Timeframe = timeframe }; } diff --git a/src/Managing.Infrastructure.Web3/Subgraphs/Gbc.cs b/src/Managing.Infrastructure.Web3/Subgraphs/Gbc.cs index f726eb42..0f8b17a4 100644 --- a/src/Managing.Infrastructure.Web3/Subgraphs/Gbc.cs +++ b/src/Managing.Infrastructure.Web3/Subgraphs/Gbc.cs @@ -29,12 +29,14 @@ public class Gbc : ISubgraphPrices var unixTimeframe = timeframe.GetUnixInterval(); var start = startDate.ToUnixTimestamp(); var end = DateTime.UtcNow.ToUnixTimestamp(); - var feedCondition = $@"{{ tokenAddress: ""_{tickerContract}"", interval: ""_{unixTimeframe}"", timestamp_gte: {start}, timestamp_lte: {end} }}"; + var feedCondition = + $@"{{ tokenAddress: ""_{tickerContract}"", interval: ""_{unixTimeframe}"", timestamp_gte: {start}, timestamp_lte: {end} }}"; // Fetching prices from graphql ticker for (int i = 0; i < batchMax; i++) { - var query = $"{{ pricefeeds(first: {batchSize}, skip: {i * batchSize}, orderBy: timestamp, orderDirection: desc, where: {feedCondition} ) {{ timestamp,o,h,l,c}} }}"; + var query = + $"{{ pricefeeds(first: {batchSize}, skip: {i * batchSize}, orderBy: timestamp, orderDirection: desc, where: {feedCondition} ) {{ timestamp,o,h,l,c}} }}"; var graphQuery = new GraphQLRequest { Query = query @@ -75,7 +77,7 @@ public class Gbc : ISubgraphPrices Low = FormatPrice(ohlc.L), Close = FormatPrice(ohlc.C), Exchange = TradingExchanges.Evm, - Ticker = ticker.ToString(), + Ticker = ticker, Timeframe = timeframe }; } @@ -92,7 +94,8 @@ public class Gbc : ISubgraphPrices public Task> GetTickers() { - var tickers = new List() { + var tickers = new List() + { Ticker.BTC, Ticker.LINK, Ticker.ETH, @@ -101,4 +104,4 @@ public class Gbc : ISubgraphPrices return Task.FromResult(tickers.AsEnumerable()); } -} +} \ No newline at end of file diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 8bb1889e..857594f6 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -331,6 +331,41 @@ export class AccountClient extends AuthorizedApiBase { } return Promise.resolve(null as any); } + + account_GetExchangeApprovalStatus(): Promise { + let url_ = this.baseUrl + "/Account/exchange-approval-status"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processAccount_GetExchangeApprovalStatus(_response); + }); + } + + protected processAccount_GetExchangeApprovalStatus(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ExchangeApprovalStatus[]; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } } export class BacktestClient extends AuthorizedApiBase { @@ -3535,6 +3570,7 @@ export interface Account { secret?: string | null; user?: User | null; balances?: Balance[] | null; + isGmxInitialized?: boolean; isPrivyWallet?: boolean; } @@ -3735,6 +3771,11 @@ export interface SendTokenRequest { chainId?: number | null; } +export interface ExchangeApprovalStatus { + exchange?: TradingExchanges; + isApproved?: boolean; +} + export interface Backtest { id: string; finalPnl: number; @@ -3984,7 +4025,7 @@ export enum Confidence { export interface Candle { exchange: TradingExchanges; - ticker: string; + ticker: Ticker; openTime: Date; date: Date; open: number; @@ -4073,7 +4114,7 @@ export interface TradingBotConfigRequest { botTradingBalance: number; name: string; flipPosition: boolean; - cooldownPeriod?: number; + cooldownPeriod?: number | null; maxLossStreak?: number; scenario?: ScenarioRequest | null; scenarioName?: string | null; @@ -4625,6 +4666,8 @@ export interface PrivyInitAddressResponse { orderVaultHash?: string | null; exchangeRouterHash?: string | null; error?: string | null; + address?: string | null; + isAlreadyInitialized?: boolean; } export interface LoginRequest { diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index 3acabd3d..083ee1d9 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -18,6 +18,7 @@ export interface Account { secret?: string | null; user?: User | null; balances?: Balance[] | null; + isGmxInitialized?: boolean; isPrivyWallet?: boolean; } @@ -218,6 +219,11 @@ export interface SendTokenRequest { chainId?: number | null; } +export interface ExchangeApprovalStatus { + exchange?: TradingExchanges; + isApproved?: boolean; +} + export interface Backtest { id: string; finalPnl: number; @@ -467,7 +473,7 @@ export enum Confidence { export interface Candle { exchange: TradingExchanges; - ticker: string; + ticker: Ticker; openTime: Date; date: Date; open: number; @@ -556,7 +562,7 @@ export interface TradingBotConfigRequest { botTradingBalance: number; name: string; flipPosition: boolean; - cooldownPeriod?: number; + cooldownPeriod?: number | null; maxLossStreak?: number; scenario?: ScenarioRequest | null; scenarioName?: string | null; @@ -1108,6 +1114,8 @@ export interface PrivyInitAddressResponse { orderVaultHash?: string | null; exchangeRouterHash?: string | null; error?: string | null; + address?: string | null; + isAlreadyInitialized?: boolean; } export interface LoginRequest { diff --git a/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx b/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx index dd494640..e2dfbe21 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx @@ -2,12 +2,12 @@ import React, {useState} from 'react' import {Card, FormInput, GridTile} from '../../components/mollecules' import useApiUrlStore from '../../app/store/apiStore' import { - type AgentBalanceHistory, - BotStatus, - DataClient, - type Position, - TradeDirection, - type UserStrategyDetailsViewModel + type AgentBalanceHistory, + BotStatus, + DataClient, + type Position, + TradeDirection, + type UserStrategyDetailsViewModel } from '../../generated/ManagingApi' interface AgentData { @@ -140,7 +140,7 @@ function AgentSearch({ index }: { index: number }) { totalWins, totalLosses, avgWinRate, - activeStrategies: agentData.strategies.filter(s => s.state === 'RUNNING').length, + activeStrategies: agentData.strategies.filter(s => s.state === BotStatus.Running).length, totalStrategies: agentData.strategies.length } } diff --git a/src/Managing.WebApp/src/pages/settingsPage/account/accountRowDetails.tsx b/src/Managing.WebApp/src/pages/settingsPage/account/accountRowDetails.tsx index 3295d604..0ad2c633 100644 --- a/src/Managing.WebApp/src/pages/settingsPage/account/accountRowDetails.tsx +++ b/src/Managing.WebApp/src/pages/settingsPage/account/accountRowDetails.tsx @@ -1,10 +1,12 @@ import React, {useState} from 'react' import {FiRefreshCw, FiSend} from 'react-icons/fi' +import {useQuery} from '@tanstack/react-query' import {SelectColumnFilter, Table} from '../../../components/mollecules' import type {IAccountRowDetail} from '../../../global/type.tsx' import type {Account, Balance} from '../../../generated/ManagingApi' -import {Ticker} from '../../../generated/ManagingApi' +import {AccountClient, Ticker} from '../../../generated/ManagingApi' +import useApiUrlStore from '../../../app/store/apiStore' import SwapModal from './SwapModal' import SendTokenModal from './SendTokenModal' @@ -17,6 +19,21 @@ const AccountRowDetails: React.FC = ({ showTotal, account, }) => { + const { apiUrl } = useApiUrlStore() + const accountClient = new AccountClient({}, apiUrl) + + // Fetch exchange approval status using TanStack Query + const { data: exchangeApprovalStatus, isLoading: isLoadingApprovalStatus, error: approvalStatusError, refetch: refetchApprovalStatus } = useQuery({ + queryKey: ['exchangeApprovalStatus'], + queryFn: async () => { + return await accountClient.account_GetExchangeApprovalStatus() + }, + staleTime: 60000, // Consider data fresh for 1 minute + gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes + retry: 2, // Retry failed requests up to 2 times + enabled: !!apiUrl, // Only run query when apiUrl is available + }) + const [swapModalState, setSwapModalState] = useState<{ isOpen: boolean fromTicker: Ticker | null @@ -164,6 +181,44 @@ const AccountRowDetails: React.FC = ({ showTotal={showTotal} showPagination={false} /> + + {/* Exchange Approval Status */} +
+
+

Exchange Approval Status

+ +
+ {isLoadingApprovalStatus ? ( +
Loading approval status...
+ ) : approvalStatusError ? ( +
Error loading approval status
+ ) : exchangeApprovalStatus && exchangeApprovalStatus.length > 0 ? ( +
+ {exchangeApprovalStatus.map((status) => ( +
+ {status.exchange}: {status.isApproved ? 'Approved' : 'Not Approved'} +
+ ))} +
+ ) : ( +
No exchange data available
+ )} +
{swapModalState.isOpen && swapModalState.fromTicker && (