Price reminder and init approval
* Start price reminder grain * Add config and init grain at startup * Save init wallet when already init
This commit is contained in:
@@ -147,5 +147,18 @@ namespace Managing.Api.Controllers
|
||||
var user = await GetUser();
|
||||
return Ok(_AccountService.DeleteAccount(user, name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <returns>A list of exchange approval statuses.</returns>
|
||||
[HttpGet("exchange-approval-status")]
|
||||
public async Task<ActionResult<List<ExchangeApprovalStatus>>> GetExchangeApprovalStatus()
|
||||
{
|
||||
var user = await GetUser();
|
||||
var exchangeStatuses = await _AccountService.GetExchangeApprovalStatusAsync(user);
|
||||
return Ok(exchangeStatuses);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<CandleHub> _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<CandleHub> 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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -313,7 +313,6 @@ app.UseEndpoints(endpoints =>
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapHub<BotHub>("/bothub");
|
||||
endpoints.MapHub<BacktestHub>("/backtesthub");
|
||||
endpoints.MapHub<CandleHub>("/candlehub");
|
||||
|
||||
endpoints.MapHealthChecks("/health", new HealthCheckOptions
|
||||
{
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using Managing.Domain.Candles;
|
||||
using Orleans;
|
||||
|
||||
namespace Managing.Application.Abstractions.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface ICandleStoreGrain : IGrainWithStringKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current list of historical candles (up to 500 most recent)
|
||||
/// </summary>
|
||||
/// <returns>List of candles ordered by date</returns>
|
||||
Task<List<Candle>> GetCandlesAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using Orleans;
|
||||
|
||||
namespace Managing.Application.Abstractions.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IPriceFetcher1DayGrain : IGrainWithIntegerKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetches daily price data for all supported exchange/ticker combinations
|
||||
/// and publishes new candles to their respective streams.
|
||||
/// </summary>
|
||||
/// <returns>True if the operation completed successfully, false otherwise</returns>
|
||||
Task<bool> FetchAndPublishPricesAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using Orleans;
|
||||
|
||||
namespace Managing.Application.Abstractions.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IPriceFetcher1HourGrain : IGrainWithIntegerKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetches 1-hour price data for all supported exchange/ticker combinations
|
||||
/// and publishes new candles to their respective streams.
|
||||
/// </summary>
|
||||
/// <returns>True if the operation completed successfully, false otherwise</returns>
|
||||
Task<bool> FetchAndPublishPricesAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using Orleans;
|
||||
|
||||
namespace Managing.Application.Abstractions.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IPriceFetcher4HourGrain : IGrainWithIntegerKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetches 4-hour price data for all supported exchange/ticker combinations
|
||||
/// and publishes new candles to their respective streams.
|
||||
/// </summary>
|
||||
/// <returns>True if the operation completed successfully, false otherwise</returns>
|
||||
Task<bool> FetchAndPublishPricesAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using Orleans;
|
||||
|
||||
namespace Managing.Application.Abstractions.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public partial interface IPriceFetcher5MinGrain : IGrainWithIntegerKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetches 5-minute price data for all supported exchange/ticker combinations
|
||||
/// and publishes new candles to their respective streams.
|
||||
/// </summary>
|
||||
/// <returns>True if the operation completed successfully, false otherwise</returns>
|
||||
Task<bool> FetchAndPublishPricesAsync();
|
||||
}
|
||||
@@ -7,6 +7,7 @@ public interface IAccountRepository
|
||||
Task<Account> GetAccountByNameAsync(string name);
|
||||
Task<Account> GetAccountByKeyAsync(string key);
|
||||
Task InsertAccountAsync(Account account);
|
||||
Task UpdateAccountAsync(Account account);
|
||||
void DeleteAccountByName(string name);
|
||||
Task<IEnumerable<Account>> GetAccountsAsync();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public interface IEvmManager
|
||||
decimal GetVolume(SubgraphProvider subgraphProvider, Ticker ticker);
|
||||
Task<List<Ticker>> GetAvailableTicker();
|
||||
Task<Candle> GetCandle(Ticker ticker);
|
||||
Task<PrivyInitAddressResponse> InitAddress(string publicAddress);
|
||||
Task<PrivyInitAddressResponse> InitAddressForGMX(string publicAddress);
|
||||
|
||||
Task<bool> Send(Chain chain, Ticker ticker, decimal amount, string publicAddress, string privateKey,
|
||||
string receiverAddress);
|
||||
|
||||
@@ -34,4 +34,6 @@ public interface IAccountService
|
||||
|
||||
Task<SwapInfos> SendTokenAsync(User user, string accountName, string recipientAddress, Ticker ticker,
|
||||
decimal amount, int? chainId = null);
|
||||
|
||||
Task<List<ExchangeApprovalStatus>> GetExchangeApprovalStatusAsync(User user);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Managing.Application.Abstractions.Services;
|
||||
|
||||
public interface IStreamService
|
||||
{
|
||||
Task SubscribeCandle();
|
||||
Task UnSubscribeCandle();
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public interface ITradingService
|
||||
Task<IEnumerable<Position>> GetAllDatabasePositionsAsync();
|
||||
Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier);
|
||||
Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifiersAsync(IEnumerable<Guid> initiatorIdentifiers);
|
||||
Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress);
|
||||
Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress, TradingExchanges tradingExchange);
|
||||
|
||||
// Synth API integration methods
|
||||
Task<SignalValidationResult> ValidateSynthSignalAsync(LightSignal signal, decimal currentPrice,
|
||||
|
||||
@@ -333,6 +333,34 @@ public class AccountService : IAccountService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<ExchangeApprovalStatus>> GetExchangeApprovalStatusAsync(User user)
|
||||
{
|
||||
var accounts = await GetAccountsByUserAsync(user, hideSecrets: true, getBalance: false);
|
||||
|
||||
var exchangeStatuses = new List<ExchangeApprovalStatus>();
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
215
src/Managing.Application/Grains/CandleStoreGrain.cs
Normal file
215
src/Managing.Application/Grains/CandleStoreGrain.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Grain for managing in-memory historical candle data with Orleans state persistence.
|
||||
/// Subscribes to price streams and maintains a rolling window of 500 candles.
|
||||
/// </summary>
|
||||
public class CandleStoreGrain : Grain, ICandleStoreGrain, IAsyncObserver<Candle>
|
||||
{
|
||||
private readonly IPersistentState<CandleStoreGrainState> _state;
|
||||
private readonly ILogger<CandleStoreGrain> _logger;
|
||||
private readonly ICandleRepository _candleRepository;
|
||||
|
||||
private const int MaxCandleCount = 500;
|
||||
private IAsyncStream<Candle> _priceStream;
|
||||
private StreamSubscriptionHandle<Candle> _streamSubscription;
|
||||
|
||||
public CandleStoreGrain(
|
||||
[PersistentState("candle-store-state", "candle-store")]
|
||||
IPersistentState<CandleStoreGrainState> state,
|
||||
ILogger<CandleStoreGrain> 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<TradingExchanges>(parts[0], out var exchange) ||
|
||||
!Enum.TryParse<Ticker>(parts[1], out var ticker) ||
|
||||
!Enum.TryParse<Timeframe>(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<List<Candle>> GetCandlesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Task.FromResult(_state.State.Candles?.ToList() ?? new List<Candle>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving candles for grain {GrainKey}", this.GetPrimaryKeyString());
|
||||
return Task.FromResult(new List<Candle>());
|
||||
}
|
||||
}
|
||||
|
||||
// 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<Candle>();
|
||||
}
|
||||
|
||||
// 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<Candle>();
|
||||
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<Candle>();
|
||||
await _state.WriteStateAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubscribeToPriceStreamAsync(string streamKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var streamProvider = this.GetStreamProvider("DefaultStreamProvider");
|
||||
_priceStream = streamProvider.GetStream<Candle>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// State object for CandleStoreGrain containing the rolling window of candles
|
||||
/// </summary>
|
||||
[GenerateSerializer]
|
||||
public class CandleStoreGrainState
|
||||
{
|
||||
[Id(0)]
|
||||
public List<Candle> Candles { get; set; } = new();
|
||||
}
|
||||
172
src/Managing.Application/Grains/PriceFetcher5MinGrain.cs
Normal file
172
src/Managing.Application/Grains/PriceFetcher5MinGrain.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[StatelessWorker]
|
||||
public class PriceFetcher5MinGrain : Grain, IPriceFetcher5MinGrain, IRemindable
|
||||
{
|
||||
private readonly ILogger<PriceFetcher5MinGrain> _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<PriceFetcher5MinGrain> 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<bool> FetchAndPublishPricesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Starting 5-minute price fetch cycle");
|
||||
|
||||
var fetchTasks = new List<Task>();
|
||||
|
||||
// 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<Candle>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Managing.Application/Grains/PriceFetcherInitializer.cs
Normal file
25
src/Managing.Application/Grains/PriceFetcherInitializer.cs
Normal file
@@ -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<IPriceFetcher5MinGrain>(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1"/>
|
||||
<PackageReference Include="Microsoft.Orleans.Reminders" Version="9.2.1"/>
|
||||
<PackageReference Include="Microsoft.Orleans.Runtime" Version="9.2.1"/>
|
||||
<PackageReference Include="Microsoft.Orleans.Streaming" Version="9.2.1"/>
|
||||
<PackageReference Include="Polly" Version="8.4.0"/>
|
||||
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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<CandleHub> _hubContext;
|
||||
|
||||
|
||||
public StreamService(IExchangeStream exchangeStream, IHubContext<CandleHub> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<TradingService> 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<string> PositionIdentifiers { get; set; }
|
||||
}
|
||||
|
||||
public async Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress)
|
||||
public async Task<PrivyInitAddressResponse> 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)
|
||||
{
|
||||
|
||||
@@ -66,9 +66,16 @@ public static class ApiBootstrap
|
||||
.AddWorkers(configuration)
|
||||
.AddFluentValidation()
|
||||
.AddMediatR()
|
||||
.AddHostedServices()
|
||||
;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddHostedServices(this IServiceCollection services)
|
||||
{
|
||||
// services.AddHostedService<PriceFetcherInitializer>();
|
||||
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.
|
||||
@@ -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<IAccountService, AccountService>();
|
||||
services.AddTransient<ITradingService, TradingService>();
|
||||
services.AddTransient<IMessengerService, MessengerService>();
|
||||
services.AddTransient<ICandleRepository, CandleRepository>();
|
||||
});
|
||||
})
|
||||
;
|
||||
@@ -347,7 +365,6 @@ public static class ApiBootstrap
|
||||
|
||||
services.AddTransient<ITradaoService, TradaoService>();
|
||||
services.AddTransient<IExchangeService, ExchangeService>();
|
||||
services.AddTransient<IExchangeStream, ExchangeStream>();
|
||||
|
||||
|
||||
services.AddTransient<IPrivyService, PrivyService>();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<PackageReference Include="Microsoft.Orleans.Persistence.AdoNet" Version="9.2.1"/>
|
||||
<PackageReference Include="Microsoft.Orleans.Reminders.AdoNet" Version="9.2.1"/>
|
||||
<PackageReference Include="Microsoft.Orleans.Server" Version="9.2.1"/>
|
||||
<PackageReference Include="Microsoft.Orleans.Streaming" Version="9.2.1"/>
|
||||
<PackageReference Include="OrleansDashboard" Version="8.2.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Managing.Common
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Common
|
||||
{
|
||||
public class Constants
|
||||
{
|
||||
@@ -65,19 +67,39 @@
|
||||
{
|
||||
public const string OracleKeeperUrl = "https://arbitrum-v2-1-api.gmxinfra.io";
|
||||
|
||||
public static readonly HashSet<Enums.Ticker> DeltaNeutralTickers = new()
|
||||
public static readonly HashSet<Ticker> 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
|
||||
|
||||
@@ -29,5 +29,8 @@ public class Account
|
||||
[Id(6)]
|
||||
public List<Balance> Balances { get; set; }
|
||||
|
||||
[Id(7)]
|
||||
public bool IsGmxInitialized { get; set; } = false;
|
||||
|
||||
public bool IsPrivyWallet => Type == AccountType.Privy;
|
||||
}
|
||||
14
src/Managing.Domain/Accounts/ExchangeApprovalStatus.cs
Normal file
14
src/Managing.Domain/Accounts/ExchangeApprovalStatus.cs
Normal file
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -114,7 +114,7 @@ public class StDevContext : IndicatorBase
|
||||
Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(
|
||||
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker),
|
||||
candleSignal.Ticker,
|
||||
direction,
|
||||
confidence,
|
||||
candleSignal,
|
||||
|
||||
@@ -108,7 +108,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
|
||||
Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(
|
||||
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker),
|
||||
candleSignal.Ticker,
|
||||
direction,
|
||||
confidence,
|
||||
candleSignal,
|
||||
|
||||
@@ -105,7 +105,7 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
|
||||
private void AddSignal(CandleDualEma candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(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))
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
|
||||
|
||||
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(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))
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
|
||||
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(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))
|
||||
{
|
||||
|
||||
@@ -125,7 +125,7 @@ public class LaggingSTC : IndicatorBase
|
||||
private void AddSignal(CandleSct candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(
|
||||
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker),
|
||||
candleSignal.Ticker,
|
||||
direction,
|
||||
confidence,
|
||||
candleSignal,
|
||||
|
||||
@@ -105,7 +105,7 @@ public class MacdCrossIndicatorBase : IndicatorBase
|
||||
private void AddSignal(CandleMacd candleSignal, TradeDirection direction,
|
||||
Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(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))
|
||||
{
|
||||
|
||||
@@ -233,7 +233,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
|
||||
|
||||
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(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))
|
||||
{
|
||||
|
||||
@@ -206,7 +206,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
|
||||
|
||||
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, HashSet<Candle> candles)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(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)
|
||||
|
||||
@@ -106,7 +106,7 @@ public class StcIndicatorBase : IndicatorBase
|
||||
private void AddSignal(CandleSct candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(
|
||||
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker),
|
||||
candleSignal.Ticker,
|
||||
direction,
|
||||
confidence,
|
||||
candleSignal,
|
||||
|
||||
@@ -170,7 +170,7 @@ public class SuperTrendCrossEma : IndicatorBase
|
||||
|
||||
private void AddSignal(CandleSuperTrend candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(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))
|
||||
|
||||
@@ -107,7 +107,7 @@ public class SuperTrendIndicatorBase : IndicatorBase
|
||||
|
||||
private void AddSignal(CandleSuperTrend candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(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))
|
||||
|
||||
@@ -66,7 +66,7 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase
|
||||
|
||||
public void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(MiscExtensions.ParseEnum<Ticker>(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))
|
||||
{
|
||||
|
||||
@@ -102,7 +102,7 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
|
||||
private void AddSignal(CandleStochRsi candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new LightSignal(
|
||||
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker),
|
||||
candleSignal.Ticker,
|
||||
direction,
|
||||
confidence,
|
||||
candleSignal,
|
||||
|
||||
@@ -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<Ticker>(data.Ticker),
|
||||
return ComputeSignals(lightScenario, latestSignalsPerIndicator, data.Ticker,
|
||||
data.Timeframe, config);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TradingExchanges>(dto.Exchange),
|
||||
Ticker = dto.Ticker,
|
||||
Ticker = MiscExtensions.ParseEnum<Ticker>(dto.Ticker),
|
||||
OpenTime = dto.OpenTime,
|
||||
Open = dto.Open,
|
||||
Close = dto.Close,
|
||||
|
||||
1440
src/Managing.Infrastructure.Database/Migrations/20250912190732_AddIsGmxInitializedToAccount.Designer.cs
generated
Normal file
1440
src/Managing.Infrastructure.Database/Migrations/20250912190732_AddIsGmxInitializedToAccount.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Managing.Infrastructure.Databases.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsGmxInitializedToAccount : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsGmxInitialized",
|
||||
table: "Accounts",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsGmxInitialized",
|
||||
table: "Accounts");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,11 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsGmxInitialized")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -64,6 +64,9 @@ public class ManagingDbContext : DbContext
|
||||
entity.Property(e => e.Type)
|
||||
.IsRequired()
|
||||
.HasConversion<string>(); // Store enum as string
|
||||
entity.Property(e => e.IsGmxInitialized)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false); // Default value for new records
|
||||
|
||||
// Create unique index on account name
|
||||
entity.HasIndex(e => e.Name).IsUnique();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Balance>() // Empty list for now, balances handled separately if needed
|
||||
Balances = new List<Balance>(), // 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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Managing.Infrastructure.Exchanges
|
||||
{
|
||||
return new Candle()
|
||||
{
|
||||
Ticker = ticker.ToString(),
|
||||
Ticker = ticker,
|
||||
Timeframe = timeframe,
|
||||
Volume = candle.Volume,
|
||||
Close = candle.ClosePrice,
|
||||
|
||||
@@ -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<StreamService> logger;
|
||||
private readonly IBinanceSocketClient _binanceSocketClient;
|
||||
|
||||
public ExchangeStream(IBinanceSocketClient binanceSocketClient, ILogger<StreamService> logger)
|
||||
{
|
||||
_binanceSocketClient = binanceSocketClient;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartBinanceWorker(Ticker ticker, Func<Candle, Task> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<Ticker>(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<BinanceFuturesPlacedOrder> 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<BinanceUsdFuturesOrder> binanceResult, decimal? leverage)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class BinanceFuturesPlacedOrder
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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<FTXOrder> 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<Ticker>(data.Symbol),
|
||||
data.Quantity, data.AverageFillPrice ?? 0, leverage,
|
||||
data.ClientOrderId, "");
|
||||
}
|
||||
|
||||
internal static Trade Map(WebCallResult<FTXTriggerOrder> 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<Ticker>(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<Ticker>(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> ftxOrderBook)
|
||||
{
|
||||
return new Orderbook()
|
||||
{
|
||||
Asks = Map(ftxOrderBook.Data.Asks),
|
||||
Bids = Map(ftxOrderBook.Data.Bids)
|
||||
};
|
||||
}
|
||||
|
||||
private static List<OrderBookEntry> Map(IEnumerable<FTXOrderBookEntry> entry)
|
||||
{
|
||||
return entry.Select(ask => new OrderBookEntry() { Price = ask.Price, Quantity = ask.Quantity }).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Ticker>(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<string, KrakenOrder> o)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,21 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="9.9.7" />
|
||||
<PackageReference Include="CryptoExchange.Net" Version="7.5.2" />
|
||||
<PackageReference Include="FTX.Net" Version="1.0.16" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.6.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||
<PackageReference Include="Binance.Net" Version="9.9.7"/>
|
||||
<PackageReference Include="CryptoExchange.Net" Version="7.5.2"/>
|
||||
<PackageReference Include="FTX.Net" Version="1.0.16"/>
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.6.5"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj" />
|
||||
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj" />
|
||||
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Helpers\"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -506,7 +506,7 @@ public class EvmManager : IEvmManager
|
||||
return lastCandles.Last();
|
||||
}
|
||||
|
||||
public async Task<PrivyInitAddressResponse> InitAddress(string publicAddress)
|
||||
public async Task<PrivyInitAddressResponse> InitAddressForGMX(string publicAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ public static class PriceExtensions
|
||||
{
|
||||
public static List<Candle> 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, int>
|
||||
{
|
||||
{ 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<IEnumerable<Ticker>> GetTickers()
|
||||
{
|
||||
var tickers = new List<Ticker>() {
|
||||
var tickers = new List<Ticker>()
|
||||
{
|
||||
Ticker.BTC,
|
||||
Ticker.LINK,
|
||||
Ticker.ETH,
|
||||
|
||||
@@ -331,6 +331,41 @@ export class AccountClient extends AuthorizedApiBase {
|
||||
}
|
||||
return Promise.resolve<SwapInfos>(null as any);
|
||||
}
|
||||
|
||||
account_GetExchangeApprovalStatus(): Promise<ExchangeApprovalStatus[]> {
|
||||
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<ExchangeApprovalStatus[]> {
|
||||
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<ExchangeApprovalStatus[]>(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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IAccountRowDetailProps> = ({
|
||||
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
|
||||
@@ -165,6 +182,44 @@ const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
|
||||
showPagination={false}
|
||||
/>
|
||||
|
||||
{/* Exchange Approval Status */}
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-sm font-medium text-gray-700">Exchange Approval Status</h4>
|
||||
<button
|
||||
className={`btn btn-xs btn-ghost ${isLoadingApprovalStatus ? 'loading' : ''}`}
|
||||
onClick={() => refetchApprovalStatus()}
|
||||
disabled={isLoadingApprovalStatus}
|
||||
title="Refresh approval status"
|
||||
>
|
||||
{!isLoadingApprovalStatus && <FiRefreshCw className="h-3 w-3" />}
|
||||
{!isLoadingApprovalStatus && <span className="ml-1">Refresh</span>}
|
||||
</button>
|
||||
</div>
|
||||
{isLoadingApprovalStatus ? (
|
||||
<div className="text-sm text-gray-500">Loading approval status...</div>
|
||||
) : approvalStatusError ? (
|
||||
<div className="text-sm text-red-500">Error loading approval status</div>
|
||||
) : exchangeApprovalStatus && exchangeApprovalStatus.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{exchangeApprovalStatus.map((status) => (
|
||||
<div
|
||||
key={status.exchange}
|
||||
className={`badge ${
|
||||
status.isApproved
|
||||
? 'badge-success'
|
||||
: 'badge-outline'
|
||||
}`}
|
||||
>
|
||||
{status.exchange}: {status.isApproved ? 'Approved' : 'Not Approved'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500">No exchange data available</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{swapModalState.isOpen && swapModalState.fromTicker && (
|
||||
<SwapModal
|
||||
isOpen={swapModalState.isOpen}
|
||||
|
||||
Reference in New Issue
Block a user