From 56b4f14eb3dd20723b48b2379c798915d41da932 Mon Sep 17 00:00:00 2001
From: Oda <102867384+CryptoOda@users.noreply.github.com>
Date: Sat, 13 Sep 2025 02:29:14 +0700
Subject: [PATCH] Price reminder and init approval
* Start price reminder grain
* Add config and init grain at startup
* Save init wallet when already init
---
.../Controllers/AccountController.cs | 13 +
.../Controllers/DataController.cs | 5 -
.../Controllers/TradingController.cs | 2 +-
src/Managing.Api/Program.cs | 1 -
.../Grains/ICandleStoreGrain.cs | 19 +
.../Grains/IPriceFetcher1DayGrain.cs | 19 +
.../Grains/IPriceFetcher1HourGrain.cs | 19 +
.../Grains/IPriceFetcher4HourGrain.cs | 19 +
.../Grains/IPriceFetcher5MinGrain.cs | 18 +
.../Repositories/IAccountRepository.cs | 1 +
.../Repositories/IEvmManager.cs | 2 +-
.../Services/IAccountService.cs | 2 +
.../Services/IStreamService.cs | 7 -
.../Services/ITradingService.cs | 2 +-
.../Accounts/AccountService.cs | 28 +
.../Bots/TradingBotBase.cs | 2 +-
.../Grains/CandleStoreGrain.cs | 215 +++
.../Grains/PriceFetcher5MinGrain.cs | 172 ++
.../Grains/PriceFetcherInitializer.cs | 25 +
src/Managing.Application/Hubs/CandleHub.cs | 41 -
.../Managing.Application.csproj | 1 +
.../Shared/StreamService.cs | 30 -
.../Trading/TradingService.cs | 41 +-
src/Managing.Bootstrap/ApiBootstrap.cs | 51 +-
.../Managing.Bootstrap.csproj | 1 +
src/Managing.Common/Constants.cs | 48 +-
src/Managing.Domain/Accounts/Account.cs | 3 +
.../Accounts/ExchangeApprovalStatus.cs | 14 +
src/Managing.Domain/Candles/Candle.cs | 8 +-
.../Evm/PrivyInitAddressResponse.cs | 2 +
.../Indicators/Context/StDevContext.cs | 2 +-
.../Signals/ChandelierExitIndicatorBase.cs | 2 +-
.../Signals/DualEmaCrossIndicatorBase.cs | 2 +-
.../Indicators/Signals/EmaCrossIndicator.cs | 2 +-
.../Signals/EmaCrossIndicatorBase.cs | 2 +-
.../Indicators/Signals/LaggingSTC.cs | 2 +-
.../Signals/MacdCrossIndicatorBase.cs | 2 +-
.../RsiDivergenceConfirmIndicatorBase.cs | 2 +-
.../Signals/RsiDivergenceIndicatorBase.cs | 2 +-
.../Indicators/Signals/StcIndicatorBase.cs | 2 +-
.../Indicators/Signals/SuperTrendCrossEma.cs | 2 +-
.../Signals/SuperTrendIndicatorBase.cs | 2 +-
.../Trends/EmaTrendIndicatorBase.cs | 2 +-
.../Trends/StochRsiTrendIndicatorBase.cs | 2 +-
.../Shared/Helpers/TradingBox.cs | 5 +-
.../InfluxDb/PriceHelpers.cs | 4 +-
...2_AddIsGmxInitializedToAccount.Designer.cs | 1440 +++++++++++++++++
...0912190732_AddIsGmxInitializedToAccount.cs | 29 +
.../ManagingDbContextModelSnapshot.cs | 5 +
.../PostgreSql/Entities/AccountEntity.cs | 3 +-
.../PostgreSql/ManagingDbContext.cs | 3 +
.../PostgreSql/PostgreSqlAccountRepository.cs | 32 +-
.../PostgreSql/PostgreSqlMappers.cs | 4 +-
.../CandleHelpers.cs | 2 +-
.../ExchangeStream.cs | 41 -
.../Helpers/BinanceHelpers.cs | 186 ---
.../Helpers/FtxHelpers.cs | 239 ---
.../Helpers/KrakenHelpers.cs | 46 -
.../Managing.Infrastructure.Exchanges.csproj | 38 +-
.../EvmManagerTests.cs | 2 +-
.../EvmManager.cs | 2 +-
.../Extensions/PriceExtensions.cs | 7 +-
.../Services/Gmx/GmxMappers.cs | 2 +-
.../Services/Gmx/GmxV2Mappers.cs | 2 +-
.../Subgraphs/Gbc.cs | 13 +-
.../src/generated/ManagingApi.ts | 47 +-
.../src/generated/ManagingApiTypes.ts | 12 +-
.../src/pages/dashboardPage/agentSearch.tsx | 14 +-
.../account/accountRowDetails.tsx | 57 +-
69 files changed, 2373 insertions(+), 701 deletions(-)
create mode 100644 src/Managing.Application.Abstractions/Grains/ICandleStoreGrain.cs
create mode 100644 src/Managing.Application.Abstractions/Grains/IPriceFetcher1DayGrain.cs
create mode 100644 src/Managing.Application.Abstractions/Grains/IPriceFetcher1HourGrain.cs
create mode 100644 src/Managing.Application.Abstractions/Grains/IPriceFetcher4HourGrain.cs
create mode 100644 src/Managing.Application.Abstractions/Grains/IPriceFetcher5MinGrain.cs
delete mode 100644 src/Managing.Application.Abstractions/Services/IStreamService.cs
create mode 100644 src/Managing.Application/Grains/CandleStoreGrain.cs
create mode 100644 src/Managing.Application/Grains/PriceFetcher5MinGrain.cs
create mode 100644 src/Managing.Application/Grains/PriceFetcherInitializer.cs
delete mode 100644 src/Managing.Application/Hubs/CandleHub.cs
delete mode 100644 src/Managing.Application/Shared/StreamService.cs
create mode 100644 src/Managing.Domain/Accounts/ExchangeApprovalStatus.cs
create mode 100644 src/Managing.Infrastructure.Database/Migrations/20250912190732_AddIsGmxInitializedToAccount.Designer.cs
create mode 100644 src/Managing.Infrastructure.Database/Migrations/20250912190732_AddIsGmxInitializedToAccount.cs
delete mode 100644 src/Managing.Infrastructure.Exchanges/ExchangeStream.cs
delete mode 100644 src/Managing.Infrastructure.Exchanges/Helpers/BinanceHelpers.cs
delete mode 100644 src/Managing.Infrastructure.Exchanges/Helpers/FtxHelpers.cs
delete mode 100644 src/Managing.Infrastructure.Exchanges/Helpers/KrakenHelpers.cs
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