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:
Oda
2025-09-13 02:29:14 +07:00
committed by GitHub
parent da50b30344
commit 56b4f14eb3
69 changed files with 2373 additions and 701 deletions

View File

@@ -147,5 +147,18 @@ namespace Managing.Api.Controllers
var user = await GetUser(); var user = await GetUser();
return Ok(_AccountService.DeleteAccount(user, name)); 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);
}
} }
} }

View File

@@ -3,7 +3,6 @@ using Managing.Api.Models.Requests;
using Managing.Api.Models.Responses; using Managing.Api.Models.Responses;
using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands; using Managing.Application.ManageBot.Commands;
using Managing.Domain.Backtests; using Managing.Domain.Backtests;
using Managing.Domain.Bots; using Managing.Domain.Bots;
@@ -16,7 +15,6 @@ using Managing.Domain.Trades;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Api.Controllers; namespace Managing.Api.Controllers;
@@ -35,7 +33,6 @@ public class DataController : ControllerBase
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
private readonly IStatisticService _statisticService; private readonly IStatisticService _statisticService;
private readonly IAgentService _agentService; private readonly IAgentService _agentService;
private readonly IHubContext<CandleHub> _hubContext;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly ITradingService _tradingService; private readonly ITradingService _tradingService;
private readonly IGrainFactory _grainFactory; private readonly IGrainFactory _grainFactory;
@@ -58,7 +55,6 @@ public class DataController : ControllerBase
ICacheService cacheService, ICacheService cacheService,
IStatisticService statisticService, IStatisticService statisticService,
IAgentService agentService, IAgentService agentService,
IHubContext<CandleHub> hubContext,
IMediator mediator, IMediator mediator,
ITradingService tradingService, ITradingService tradingService,
IGrainFactory grainFactory) IGrainFactory grainFactory)
@@ -68,7 +64,6 @@ public class DataController : ControllerBase
_cacheService = cacheService; _cacheService = cacheService;
_statisticService = statisticService; _statisticService = statisticService;
_agentService = agentService; _agentService = agentService;
_hubContext = hubContext;
_mediator = mediator; _mediator = mediator;
_tradingService = tradingService; _tradingService = tradingService;
_grainFactory = grainFactory; _grainFactory = grainFactory;

View File

@@ -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."); 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); return Ok(result);
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -313,7 +313,6 @@ app.UseEndpoints(endpoints =>
endpoints.MapControllers(); endpoints.MapControllers();
endpoints.MapHub<BotHub>("/bothub"); endpoints.MapHub<BotHub>("/bothub");
endpoints.MapHub<BacktestHub>("/backtesthub"); endpoints.MapHub<BacktestHub>("/backtesthub");
endpoints.MapHub<CandleHub>("/candlehub");
endpoints.MapHealthChecks("/health", new HealthCheckOptions endpoints.MapHealthChecks("/health", new HealthCheckOptions
{ {

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -7,6 +7,7 @@ public interface IAccountRepository
Task<Account> GetAccountByNameAsync(string name); Task<Account> GetAccountByNameAsync(string name);
Task<Account> GetAccountByKeyAsync(string key); Task<Account> GetAccountByKeyAsync(string key);
Task InsertAccountAsync(Account account); Task InsertAccountAsync(Account account);
Task UpdateAccountAsync(Account account);
void DeleteAccountByName(string name); void DeleteAccountByName(string name);
Task<IEnumerable<Account>> GetAccountsAsync(); Task<IEnumerable<Account>> GetAccountsAsync();
} }

View File

@@ -27,7 +27,7 @@ public interface IEvmManager
decimal GetVolume(SubgraphProvider subgraphProvider, Ticker ticker); decimal GetVolume(SubgraphProvider subgraphProvider, Ticker ticker);
Task<List<Ticker>> GetAvailableTicker(); Task<List<Ticker>> GetAvailableTicker();
Task<Candle> GetCandle(Ticker ticker); 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, Task<bool> Send(Chain chain, Ticker ticker, decimal amount, string publicAddress, string privateKey,
string receiverAddress); string receiverAddress);

View File

@@ -34,4 +34,6 @@ public interface IAccountService
Task<SwapInfos> SendTokenAsync(User user, string accountName, string recipientAddress, Ticker ticker, Task<SwapInfos> SendTokenAsync(User user, string accountName, string recipientAddress, Ticker ticker,
decimal amount, int? chainId = null); decimal amount, int? chainId = null);
Task<List<ExchangeApprovalStatus>> GetExchangeApprovalStatusAsync(User user);
} }

View File

@@ -1,7 +0,0 @@
namespace Managing.Application.Abstractions.Services;
public interface IStreamService
{
Task SubscribeCandle();
Task UnSubscribeCandle();
}

View File

@@ -38,7 +38,7 @@ public interface ITradingService
Task<IEnumerable<Position>> GetAllDatabasePositionsAsync(); Task<IEnumerable<Position>> GetAllDatabasePositionsAsync();
Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier); Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier);
Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifiersAsync(IEnumerable<Guid> initiatorIdentifiers); Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifiersAsync(IEnumerable<Guid> initiatorIdentifiers);
Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress); Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress, TradingExchanges tradingExchange);
// Synth API integration methods // Synth API integration methods
Task<SignalValidationResult> ValidateSynthSignalAsync(LightSignal signal, decimal currentPrice, Task<SignalValidationResult> ValidateSynthSignalAsync(LightSignal signal, decimal currentPrice,

View File

@@ -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) private async Task ManagePropertiesAsync(bool hideSecrets, bool getBalance, Account account)
{ {
if (account != null) if (account != null)

View File

@@ -210,7 +210,7 @@ public class TradingBotBase : ITradingBot
Low = position.Open.Price, Low = position.Open.Price,
Volume = 0, Volume = 0,
Exchange = TradingExchanges.Evm, Exchange = TradingExchanges.Evm,
Ticker = Config.Ticker.ToString(), Ticker = Config.Ticker,
Timeframe = Config.Timeframe Timeframe = Config.Timeframe
}; };

View 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();
}

View 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();
}
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -25,6 +25,7 @@
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1"/> <PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1"/>
<PackageReference Include="Microsoft.Orleans.Reminders" 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.Runtime" Version="9.2.1"/>
<PackageReference Include="Microsoft.Orleans.Streaming" Version="9.2.1"/>
<PackageReference Include="Polly" Version="8.4.0"/> <PackageReference Include="Polly" Version="8.4.0"/>
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0"/> <PackageReference Include="Skender.Stock.Indicators" Version="2.5.0"/>
</ItemGroup> </ItemGroup>

View File

@@ -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();
}
}

View File

@@ -23,6 +23,7 @@ public class TradingService : ITradingService
private readonly ITradingRepository _tradingRepository; private readonly ITradingRepository _tradingRepository;
private readonly IExchangeService _exchangeService; private readonly IExchangeService _exchangeService;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IAccountRepository _accountRepository;
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
private readonly IMessengerService _messengerService; private readonly IMessengerService _messengerService;
private readonly IStatisticRepository _statisticRepository; private readonly IStatisticRepository _statisticRepository;
@@ -35,6 +36,7 @@ public class TradingService : ITradingService
IExchangeService exchangeService, IExchangeService exchangeService,
ILogger<TradingService> logger, ILogger<TradingService> logger,
IAccountService accountService, IAccountService accountService,
IAccountRepository accountRepository,
ICacheService cacheService, ICacheService cacheService,
IMessengerService messengerService, IMessengerService messengerService,
IStatisticRepository statisticRepository, IStatisticRepository statisticRepository,
@@ -45,6 +47,7 @@ public class TradingService : ITradingService
_exchangeService = exchangeService; _exchangeService = exchangeService;
_logger = logger; _logger = logger;
_accountService = accountService; _accountService = accountService;
_accountRepository = accountRepository;
_cacheService = cacheService; _cacheService = cacheService;
_messengerService = messengerService; _messengerService = messengerService;
_statisticRepository = statisticRepository; _statisticRepository = statisticRepository;
@@ -319,7 +322,7 @@ public class TradingService : ITradingService
$"[{shortAddress}][{ticker}] No change - Quantity still {newTrade.Quantity}"); $"[{shortAddress}][{ticker}] No change - Quantity still {newTrade.Quantity}");
} }
} }
catch (Exception ex) catch (Exception)
{ {
_logger.LogError($"[{shortAddress}][{ticker}] Impossible to fetch trader"); _logger.LogError($"[{shortAddress}][{ticker}] Impossible to fetch trader");
} }
@@ -357,7 +360,7 @@ public class TradingService : ITradingService
public List<string> PositionIdentifiers { get; set; } public List<string> PositionIdentifiers { get; set; }
} }
public async Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress) public async Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress, TradingExchanges tradingExchange)
{ {
try try
{ {
@@ -368,7 +371,39 @@ public class TradingService : ITradingService
{ Success = false, Error = "Public address cannot be null or empty" }; { 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) catch (Exception ex)
{ {

View File

@@ -66,9 +66,16 @@ public static class ApiBootstrap
.AddWorkers(configuration) .AddWorkers(configuration)
.AddFluentValidation() .AddFluentValidation()
.AddMediatR() .AddMediatR()
.AddHostedServices()
; ;
} }
private static IServiceCollection AddHostedServices(this IServiceCollection services)
{
// services.AddHostedService<PriceFetcherInitializer>();
return services;
}
// Note: IClusterClient is automatically available in co-hosting scenarios // Note: IClusterClient is automatically available in co-hosting scenarios
// through IGrainFactory. Services should inject IGrainFactory instead of IClusterClient // through IGrainFactory. Services should inject IGrainFactory instead of IClusterClient
// to avoid circular dependency issues during DI container construction. // to avoid circular dependency issues during DI container construction.
@@ -292,6 +299,11 @@ public static class ApiBootstrap
options.Invariant = "Npgsql"; options.Invariant = "Npgsql";
}) })
.AddAdoNetGrainStorage("platform-summary-store", options => .AddAdoNetGrainStorage("platform-summary-store", options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
})
.AddAdoNetGrainStorage("candle-store", options =>
{ {
options.ConnectionString = postgreSqlConnectionString; options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql"; options.Invariant = "Npgsql";
@@ -304,9 +316,14 @@ public static class ApiBootstrap
.AddMemoryGrainStorage("bot-store") .AddMemoryGrainStorage("bot-store")
.AddMemoryGrainStorage("registry-store") .AddMemoryGrainStorage("registry-store")
.AddMemoryGrainStorage("agent-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 siloBuilder
.ConfigureServices(services => .ConfigureServices(services =>
{ {
@@ -316,6 +333,7 @@ public static class ApiBootstrap
services.AddTransient<IAccountService, AccountService>(); services.AddTransient<IAccountService, AccountService>();
services.AddTransient<ITradingService, TradingService>(); services.AddTransient<ITradingService, TradingService>();
services.AddTransient<IMessengerService, MessengerService>(); services.AddTransient<IMessengerService, MessengerService>();
services.AddTransient<ICandleRepository, CandleRepository>();
}); });
}) })
; ;
@@ -347,7 +365,6 @@ public static class ApiBootstrap
services.AddTransient<ITradaoService, TradaoService>(); services.AddTransient<ITradaoService, TradaoService>();
services.AddTransient<IExchangeService, ExchangeService>(); services.AddTransient<IExchangeService, ExchangeService>();
services.AddTransient<IExchangeStream, ExchangeStream>();
services.AddTransient<IPrivyService, PrivyService>(); services.AddTransient<IPrivyService, PrivyService>();

View File

@@ -19,6 +19,7 @@
<PackageReference Include="Microsoft.Orleans.Persistence.AdoNet" Version="9.2.1"/> <PackageReference Include="Microsoft.Orleans.Persistence.AdoNet" Version="9.2.1"/>
<PackageReference Include="Microsoft.Orleans.Reminders.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.Server" Version="9.2.1"/>
<PackageReference Include="Microsoft.Orleans.Streaming" Version="9.2.1"/>
<PackageReference Include="OrleansDashboard" Version="8.2.0"/> <PackageReference Include="OrleansDashboard" Version="8.2.0"/>
</ItemGroup> </ItemGroup>

View File

@@ -1,4 +1,6 @@
namespace Managing.Common using static Managing.Common.Enums;
namespace Managing.Common
{ {
public class Constants public class Constants
{ {
@@ -65,19 +67,39 @@
{ {
public const string OracleKeeperUrl = "https://arbitrum-v2-1-api.gmxinfra.io"; 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, Ticker.BTC,
Enums.Ticker.ARB, Ticker.ARB,
Enums.Ticker.ETH, Ticker.ETH,
Enums.Ticker.BNB, Ticker.BNB,
Enums.Ticker.SOL, Ticker.SOL,
Enums.Ticker.LINK, Ticker.LINK,
Enums.Ticker.OP, Ticker.OP,
Enums.Ticker.UNI, Ticker.UNI,
Enums.Ticker.AAVE, Ticker.AAVE,
Enums.Ticker.PEPE, Ticker.PEPE,
Enums.Ticker.WIF, 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 static class Decimals

View File

@@ -29,5 +29,8 @@ public class Account
[Id(6)] [Id(6)]
public List<Balance> Balances { get; set; } public List<Balance> Balances { get; set; }
[Id(7)]
public bool IsGmxInitialized { get; set; } = false;
public bool IsPrivyWallet => Type == AccountType.Privy; public bool IsPrivyWallet => Type == AccountType.Privy;
} }

View 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; }
}

View File

@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Managing.Common;
using Orleans; using Orleans;
using Skender.Stock.Indicators; using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Candles namespace Managing.Domain.Candles
{ {
@@ -9,10 +9,10 @@ namespace Managing.Domain.Candles
public class Candle : IQuote public class Candle : IQuote
{ {
[Id(0)] [Id(0)]
[Required] public Enums.TradingExchanges Exchange { get; set; } [Required] public TradingExchanges Exchange { get; set; }
[Id(1)] [Id(1)]
[Required] public string Ticker { get; set; } [Required] public Ticker Ticker { get; set; }
[Id(2)] [Id(2)]
[Required] public DateTime OpenTime { get; set; } [Required] public DateTime OpenTime { get; set; }
@@ -33,7 +33,7 @@ namespace Managing.Domain.Candles
[Required] public decimal Low { get; set; } [Required] public decimal Low { get; set; }
[Id(8)] [Id(8)]
[Required] public Enums.Timeframe Timeframe { get; set; } [Required] public Timeframe Timeframe { get; set; }
[Id(9)] [Id(9)]
public decimal Volume { get; set; } public decimal Volume { get; set; }

View File

@@ -8,4 +8,6 @@ public class PrivyInitAddressResponse
public string? OrderVaultHash { get; set; } public string? OrderVaultHash { get; set; }
public string? ExchangeRouterHash { get; set; } public string? ExchangeRouterHash { get; set; }
public string? Error { get; set; } public string? Error { get; set; }
public string? Address { get; set; }
public bool IsAlreadyInitialized { get; set; }
} }

View File

@@ -114,7 +114,7 @@ public class StDevContext : IndicatorBase
Confidence confidence) Confidence confidence)
{ {
var signal = new LightSignal( var signal = new LightSignal(
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), candleSignal.Ticker,
direction, direction,
confidence, confidence,
candleSignal, candleSignal,

View File

@@ -108,7 +108,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
Confidence confidence) Confidence confidence)
{ {
var signal = new LightSignal( var signal = new LightSignal(
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), candleSignal.Ticker,
direction, direction,
confidence, confidence,
candleSignal, candleSignal,

View File

@@ -105,7 +105,7 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
private void AddSignal(CandleDualEma candleSignal, TradeDirection direction, Confidence confidence) 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); candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier)) if (!Signals.Any(s => s.Identifier == signal.Identifier))
{ {

View File

@@ -69,7 +69,7 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence) 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); candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier)) if (!Signals.Any(s => s.Identifier == signal.Identifier))
{ {

View File

@@ -69,7 +69,7 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence) 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); candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier)) if (!Signals.Any(s => s.Identifier == signal.Identifier))
{ {

View File

@@ -125,7 +125,7 @@ public class LaggingSTC : IndicatorBase
private void AddSignal(CandleSct candleSignal, TradeDirection direction, Confidence confidence) private void AddSignal(CandleSct candleSignal, TradeDirection direction, Confidence confidence)
{ {
var signal = new LightSignal( var signal = new LightSignal(
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), candleSignal.Ticker,
direction, direction,
confidence, confidence,
candleSignal, candleSignal,

View File

@@ -105,7 +105,7 @@ public class MacdCrossIndicatorBase : IndicatorBase
private void AddSignal(CandleMacd candleSignal, TradeDirection direction, private void AddSignal(CandleMacd candleSignal, TradeDirection direction,
Confidence confidence) 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); candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier)) if (!Signals.Any(s => s.Identifier == signal.Identifier))
{ {

View File

@@ -233,7 +233,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, Confidence confidence) 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); candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier)) if (!Signals.Any(s => s.Identifier == signal.Identifier))
{ {

View File

@@ -206,7 +206,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, HashSet<Candle> candles) 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); candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (Signals.Count(s => s.Identifier == signal.Identifier) < 1) if (Signals.Count(s => s.Identifier == signal.Identifier) < 1)

View File

@@ -106,7 +106,7 @@ public class StcIndicatorBase : IndicatorBase
private void AddSignal(CandleSct candleSignal, TradeDirection direction, Confidence confidence) private void AddSignal(CandleSct candleSignal, TradeDirection direction, Confidence confidence)
{ {
var signal = new LightSignal( var signal = new LightSignal(
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), candleSignal.Ticker,
direction, direction,
confidence, confidence,
candleSignal, candleSignal,

View File

@@ -170,7 +170,7 @@ public class SuperTrendCrossEma : IndicatorBase
private void AddSignal(CandleSuperTrend candleSignal, TradeDirection direction, Confidence confidence) 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, candleSignal.Date,
candleSignal.Exchange, Type, SignalType, Name); candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier)) if (!Signals.Any(s => s.Identifier == signal.Identifier))

View File

@@ -107,7 +107,7 @@ public class SuperTrendIndicatorBase : IndicatorBase
private void AddSignal(CandleSuperTrend candleSignal, TradeDirection direction, Confidence confidence) 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, candleSignal.Date,
candleSignal.Exchange, Type, SignalType, Name); candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier)) if (!Signals.Any(s => s.Identifier == signal.Identifier))

View File

@@ -66,7 +66,7 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase
public void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence) 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); candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier)) if (!Signals.Any(s => s.Identifier == signal.Identifier))
{ {

View File

@@ -102,7 +102,7 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
private void AddSignal(CandleStochRsi candleSignal, TradeDirection direction, Confidence confidence) private void AddSignal(CandleStochRsi candleSignal, TradeDirection direction, Confidence confidence)
{ {
var signal = new LightSignal( var signal = new LightSignal(
MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), candleSignal.Ticker,
direction, direction,
confidence, confidence,
candleSignal, candleSignal,

View File

@@ -1,5 +1,4 @@
using Managing.Core; using Managing.Domain.Candles;
using Managing.Domain.Candles;
using Managing.Domain.Indicators; using Managing.Domain.Indicators;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
@@ -124,7 +123,7 @@ public static class TradingBox
} }
var data = newCandles.First(); var data = newCandles.First();
return ComputeSignals(lightScenario, latestSignalsPerIndicator, MiscExtensions.ParseEnum<Ticker>(data.Ticker), return ComputeSignals(lightScenario, latestSignalsPerIndicator, data.Ticker,
data.Timeframe, config); data.Timeframe, config);
} }

View File

@@ -12,7 +12,7 @@ public static class PriceHelpers
var price = new PriceDto var price = new PriceDto
{ {
Exchange = candle.Exchange.ToString(), Exchange = candle.Exchange.ToString(),
Ticker = candle.Ticker, Ticker = candle.Ticker.ToString(),
OpenTime = candle.OpenTime, OpenTime = candle.OpenTime,
Open = candle.Open, Open = candle.Open,
Close = candle.Close, Close = candle.Close,
@@ -30,7 +30,7 @@ public static class PriceHelpers
return new Candle return new Candle
{ {
Exchange = MiscExtensions.ParseEnum<TradingExchanges>(dto.Exchange), Exchange = MiscExtensions.ParseEnum<TradingExchanges>(dto.Exchange),
Ticker = dto.Ticker, Ticker = MiscExtensions.ParseEnum<Ticker>(dto.Ticker),
OpenTime = dto.OpenTime, OpenTime = dto.OpenTime,
Open = dto.Open, Open = dto.Open,
Close = dto.Close, Close = dto.Close,

View File

@@ -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");
}
}
}

View File

@@ -34,6 +34,11 @@ namespace Managing.Infrastructure.Databases.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<bool>("IsGmxInitialized")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("Key") b.Property<string>("Key")
.IsRequired() .IsRequired()
.HasMaxLength(500) .HasMaxLength(500)

View File

@@ -8,12 +8,13 @@ namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
public class AccountEntity public class AccountEntity
{ {
[Key] public int Id { get; set; } [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 TradingExchanges Exchange { get; set; }
[Required] public AccountType Type { get; set; } [Required] public AccountType Type { get; set; }
[Required] public string? Key { get; set; } [Required] public string? Key { get; set; }
public string? Secret { get; set; } public string? Secret { get; set; }
[Required] public int UserId { get; set; } [Required] public int UserId { get; set; }
[Required] public bool IsGmxInitialized { get; set; } = false;
// Navigation properties // Navigation properties
public UserEntity? User { get; set; } public UserEntity? User { get; set; }

View File

@@ -64,6 +64,9 @@ public class ManagingDbContext : DbContext
entity.Property(e => e.Type) entity.Property(e => e.Type)
.IsRequired() .IsRequired()
.HasConversion<string>(); // Store enum as string .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 // Create unique index on account name
entity.HasIndex(e => e.Name).IsUnique(); entity.HasIndex(e => e.Name).IsUnique();

View File

@@ -72,7 +72,7 @@ public class PostgreSqlAccountRepository : IAccountRepository
_cacheService.SaveValue(cacheKey, account, TimeSpan.FromHours(1)); _cacheService.SaveValue(cacheKey, account, TimeSpan.FromHours(1));
return account; return account;
} }
catch (Exception ex) catch (Exception)
{ {
// If there's an error, try to reset the connection // If there's an error, try to reset the connection
throw; throw;
@@ -117,4 +117,34 @@ public class PostgreSqlAccountRepository : IAccountRepository
_context.Accounts.Add(accountEntity); _context.Accounts.Add(accountEntity);
await _context.SaveChangesAsync().ConfigureAwait(false); 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);
}
}
} }

View File

@@ -34,7 +34,8 @@ public static class PostgreSqlMappers
Key = entity.Key, Key = entity.Key,
Secret = entity.Secret, Secret = entity.Secret,
User = entity.User != null ? Map(entity.User) : null, 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, Key = account.Key,
Secret = account.Secret, Secret = account.Secret,
UserId = account.User.Id, UserId = account.User.Id,
IsGmxInitialized = account.IsGmxInitialized
}; };
} }

View File

@@ -23,7 +23,7 @@ namespace Managing.Infrastructure.Exchanges
{ {
return new Candle() return new Candle()
{ {
Ticker = ticker.ToString(), Ticker = ticker,
Timeframe = timeframe, Timeframe = timeframe,
Volume = candle.Volume, Volume = candle.Volume,
Close = candle.ClosePrice, Close = candle.ClosePrice,

View File

@@ -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();
}
}

View File

@@ -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
{
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -20,4 +20,8 @@
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/> <ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Helpers\"/>
</ItemGroup>
</Project> </Project>

View File

@@ -208,7 +208,7 @@ public class EvmManagerTests
[Fact] [Fact]
public async Task Should_Init_Address_For_Trading() public async Task Should_Init_Address_For_Trading()
{ {
var accountInitilized = await _manager.InitAddress(PublicAddress); var accountInitilized = await _manager.InitAddressForGMX(PublicAddress);
Assert.NotNull(accountInitilized); Assert.NotNull(accountInitilized);
} }

View File

@@ -506,7 +506,7 @@ public class EvmManager : IEvmManager
return lastCandles.Last(); return lastCandles.Last();
} }
public async Task<PrivyInitAddressResponse> InitAddress(string publicAddress) public async Task<PrivyInitAddressResponse> InitAddressForGMX(string publicAddress)
{ {
try try
{ {

View File

@@ -53,6 +53,7 @@ public static class PriceExtensions
h = Math.Max(o, c); h = Math.Max(o, c);
l = Math.Min(o, c); l = Math.Min(o, c);
} }
c = decimal.Parse(current.Value); c = decimal.Parse(current.Value);
h = Math.Max(h, c); h = Math.Max(h, c);
l = Math.Min(l, c); l = Math.Min(l, c);
@@ -69,7 +70,7 @@ public static class PriceExtensions
Low = x.Low, Low = x.Low,
Timeframe = x.Timeframe, Timeframe = x.Timeframe,
Exchange = TradingExchanges.Evm, Exchange = TradingExchanges.Evm,
Ticker = ticker.ToString() Ticker = ticker
}).ToList(); }).ToList();
} }
} }

View File

@@ -174,7 +174,7 @@ public static class GmxMappers
Low = Convert.ToDecimal(price.l), Low = Convert.ToDecimal(price.l),
Close = Convert.ToDecimal(price.c), Close = Convert.ToDecimal(price.c),
Exchange = TradingExchanges.Evm, Exchange = TradingExchanges.Evm,
Ticker = ticker.ToString(), Ticker = ticker,
Timeframe = timeframe Timeframe = timeframe
}; };
} }

View File

@@ -122,7 +122,7 @@ internal static class GmxV2Mappers
Low = Convert.ToDecimal(marketPrices[3]), Low = Convert.ToDecimal(marketPrices[3]),
Close = Convert.ToDecimal(marketPrices[4]), Close = Convert.ToDecimal(marketPrices[4]),
Exchange = TradingExchanges.Evm, Exchange = TradingExchanges.Evm,
Ticker = ticker.ToString(), Ticker = ticker,
Timeframe = timeframe Timeframe = timeframe
}; };
} }

View File

@@ -29,12 +29,14 @@ public class Gbc : ISubgraphPrices
var unixTimeframe = timeframe.GetUnixInterval(); var unixTimeframe = timeframe.GetUnixInterval();
var start = startDate.ToUnixTimestamp(); var start = startDate.ToUnixTimestamp();
var end = DateTime.UtcNow.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 // Fetching prices from graphql ticker
for (int i = 0; i < batchMax; i++) 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 var graphQuery = new GraphQLRequest
{ {
Query = query Query = query
@@ -75,7 +77,7 @@ public class Gbc : ISubgraphPrices
Low = FormatPrice(ohlc.L), Low = FormatPrice(ohlc.L),
Close = FormatPrice(ohlc.C), Close = FormatPrice(ohlc.C),
Exchange = TradingExchanges.Evm, Exchange = TradingExchanges.Evm,
Ticker = ticker.ToString(), Ticker = ticker,
Timeframe = timeframe Timeframe = timeframe
}; };
} }
@@ -92,7 +94,8 @@ public class Gbc : ISubgraphPrices
public Task<IEnumerable<Ticker>> GetTickers() public Task<IEnumerable<Ticker>> GetTickers()
{ {
var tickers = new List<Ticker>() { var tickers = new List<Ticker>()
{
Ticker.BTC, Ticker.BTC,
Ticker.LINK, Ticker.LINK,
Ticker.ETH, Ticker.ETH,

View File

@@ -331,6 +331,41 @@ export class AccountClient extends AuthorizedApiBase {
} }
return Promise.resolve<SwapInfos>(null as any); 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 { export class BacktestClient extends AuthorizedApiBase {
@@ -3535,6 +3570,7 @@ export interface Account {
secret?: string | null; secret?: string | null;
user?: User | null; user?: User | null;
balances?: Balance[] | null; balances?: Balance[] | null;
isGmxInitialized?: boolean;
isPrivyWallet?: boolean; isPrivyWallet?: boolean;
} }
@@ -3735,6 +3771,11 @@ export interface SendTokenRequest {
chainId?: number | null; chainId?: number | null;
} }
export interface ExchangeApprovalStatus {
exchange?: TradingExchanges;
isApproved?: boolean;
}
export interface Backtest { export interface Backtest {
id: string; id: string;
finalPnl: number; finalPnl: number;
@@ -3984,7 +4025,7 @@ export enum Confidence {
export interface Candle { export interface Candle {
exchange: TradingExchanges; exchange: TradingExchanges;
ticker: string; ticker: Ticker;
openTime: Date; openTime: Date;
date: Date; date: Date;
open: number; open: number;
@@ -4073,7 +4114,7 @@ export interface TradingBotConfigRequest {
botTradingBalance: number; botTradingBalance: number;
name: string; name: string;
flipPosition: boolean; flipPosition: boolean;
cooldownPeriod?: number; cooldownPeriod?: number | null;
maxLossStreak?: number; maxLossStreak?: number;
scenario?: ScenarioRequest | null; scenario?: ScenarioRequest | null;
scenarioName?: string | null; scenarioName?: string | null;
@@ -4625,6 +4666,8 @@ export interface PrivyInitAddressResponse {
orderVaultHash?: string | null; orderVaultHash?: string | null;
exchangeRouterHash?: string | null; exchangeRouterHash?: string | null;
error?: string | null; error?: string | null;
address?: string | null;
isAlreadyInitialized?: boolean;
} }
export interface LoginRequest { export interface LoginRequest {

View File

@@ -18,6 +18,7 @@ export interface Account {
secret?: string | null; secret?: string | null;
user?: User | null; user?: User | null;
balances?: Balance[] | null; balances?: Balance[] | null;
isGmxInitialized?: boolean;
isPrivyWallet?: boolean; isPrivyWallet?: boolean;
} }
@@ -218,6 +219,11 @@ export interface SendTokenRequest {
chainId?: number | null; chainId?: number | null;
} }
export interface ExchangeApprovalStatus {
exchange?: TradingExchanges;
isApproved?: boolean;
}
export interface Backtest { export interface Backtest {
id: string; id: string;
finalPnl: number; finalPnl: number;
@@ -467,7 +473,7 @@ export enum Confidence {
export interface Candle { export interface Candle {
exchange: TradingExchanges; exchange: TradingExchanges;
ticker: string; ticker: Ticker;
openTime: Date; openTime: Date;
date: Date; date: Date;
open: number; open: number;
@@ -556,7 +562,7 @@ export interface TradingBotConfigRequest {
botTradingBalance: number; botTradingBalance: number;
name: string; name: string;
flipPosition: boolean; flipPosition: boolean;
cooldownPeriod?: number; cooldownPeriod?: number | null;
maxLossStreak?: number; maxLossStreak?: number;
scenario?: ScenarioRequest | null; scenario?: ScenarioRequest | null;
scenarioName?: string | null; scenarioName?: string | null;
@@ -1108,6 +1114,8 @@ export interface PrivyInitAddressResponse {
orderVaultHash?: string | null; orderVaultHash?: string | null;
exchangeRouterHash?: string | null; exchangeRouterHash?: string | null;
error?: string | null; error?: string | null;
address?: string | null;
isAlreadyInitialized?: boolean;
} }
export interface LoginRequest { export interface LoginRequest {

View File

@@ -140,7 +140,7 @@ function AgentSearch({ index }: { index: number }) {
totalWins, totalWins,
totalLosses, totalLosses,
avgWinRate, avgWinRate,
activeStrategies: agentData.strategies.filter(s => s.state === 'RUNNING').length, activeStrategies: agentData.strategies.filter(s => s.state === BotStatus.Running).length,
totalStrategies: agentData.strategies.length totalStrategies: agentData.strategies.length
} }
} }

View File

@@ -1,10 +1,12 @@
import React, {useState} from 'react' import React, {useState} from 'react'
import {FiRefreshCw, FiSend} from 'react-icons/fi' import {FiRefreshCw, FiSend} from 'react-icons/fi'
import {useQuery} from '@tanstack/react-query'
import {SelectColumnFilter, Table} from '../../../components/mollecules' import {SelectColumnFilter, Table} from '../../../components/mollecules'
import type {IAccountRowDetail} from '../../../global/type.tsx' import type {IAccountRowDetail} from '../../../global/type.tsx'
import type {Account, Balance} from '../../../generated/ManagingApi' 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 SwapModal from './SwapModal'
import SendTokenModal from './SendTokenModal' import SendTokenModal from './SendTokenModal'
@@ -17,6 +19,21 @@ const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
showTotal, showTotal,
account, 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<{ const [swapModalState, setSwapModalState] = useState<{
isOpen: boolean isOpen: boolean
fromTicker: Ticker | null fromTicker: Ticker | null
@@ -165,6 +182,44 @@ const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
showPagination={false} 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 && ( {swapModalState.isOpen && swapModalState.fromTicker && (
<SwapModal <SwapModal
isOpen={swapModalState.isOpen} isOpen={swapModalState.isOpen}