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();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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> 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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>> 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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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.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>
|
||||||
|
|||||||
@@ -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 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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -90,8 +97,8 @@ public static class ApiBootstrap
|
|||||||
|
|
||||||
// Allow disabling Orleans clustering entirely in case of issues
|
// Allow disabling Orleans clustering entirely in case of issues
|
||||||
var disableOrleansClusteringEnv = Environment.GetEnvironmentVariable("DISABLE_ORLEANS_CLUSTERING");
|
var disableOrleansClusteringEnv = Environment.GetEnvironmentVariable("DISABLE_ORLEANS_CLUSTERING");
|
||||||
var disableOrleansClustering = !string.IsNullOrEmpty(disableOrleansClusteringEnv) &&
|
var disableOrleansClustering = !string.IsNullOrEmpty(disableOrleansClusteringEnv) &&
|
||||||
bool.TryParse(disableOrleansClusteringEnv, out var disabled) && disabled;
|
bool.TryParse(disableOrleansClusteringEnv, out var disabled) && disabled;
|
||||||
|
|
||||||
// Get TASK_SLOT for multi-instance configuration
|
// Get TASK_SLOT for multi-instance configuration
|
||||||
var taskSlotEnv = Environment.GetEnvironmentVariable("TASK_SLOT");
|
var taskSlotEnv = Environment.GetEnvironmentVariable("TASK_SLOT");
|
||||||
@@ -107,19 +114,19 @@ public static class ApiBootstrap
|
|||||||
var dashboardPort = 9999 + (taskSlot - 1); // 9999, 10000, 10001, etc.
|
var dashboardPort = 9999 + (taskSlot - 1); // 9999, 10000, 10001, etc.
|
||||||
|
|
||||||
// Get hostname for clustering - prioritize external IP for multi-server setups
|
// Get hostname for clustering - prioritize external IP for multi-server setups
|
||||||
var hostname = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ?? // CapRover server IP
|
var hostname = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ?? // CapRover server IP
|
||||||
Environment.GetEnvironmentVariable("EXTERNAL_IP") ?? // Custom external IP
|
Environment.GetEnvironmentVariable("EXTERNAL_IP") ?? // Custom external IP
|
||||||
Environment.GetEnvironmentVariable("HOSTNAME") ?? // Container hostname
|
Environment.GetEnvironmentVariable("HOSTNAME") ?? // Container hostname
|
||||||
Environment.GetEnvironmentVariable("COMPUTERNAME") ?? // Windows hostname
|
Environment.GetEnvironmentVariable("COMPUTERNAME") ?? // Windows hostname
|
||||||
"localhost";
|
"localhost";
|
||||||
|
|
||||||
// For Docker containers, always use localhost for same-server clustering
|
// For Docker containers, always use localhost for same-server clustering
|
||||||
IPAddress advertisedIP = IPAddress.Loopback; // Advertise as localhost for same-server clustering
|
IPAddress advertisedIP = IPAddress.Loopback; // Advertise as localhost for same-server clustering
|
||||||
|
|
||||||
// Only use external IP if specifically provided for multi-server scenarios
|
// Only use external IP if specifically provided for multi-server scenarios
|
||||||
var externalIP = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ??
|
var externalIP = Environment.GetEnvironmentVariable("CAPROVER_SERVER_IP") ??
|
||||||
Environment.GetEnvironmentVariable("EXTERNAL_IP");
|
Environment.GetEnvironmentVariable("EXTERNAL_IP");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(externalIP) && IPAddress.TryParse(externalIP, out var parsedExternalIP))
|
if (!string.IsNullOrEmpty(externalIP) && IPAddress.TryParse(externalIP, out var parsedExternalIP))
|
||||||
{
|
{
|
||||||
advertisedIP = parsedExternalIP;
|
advertisedIP = parsedExternalIP;
|
||||||
@@ -206,7 +213,7 @@ public static class ApiBootstrap
|
|||||||
options.GatewayListeningEndpoint = new IPEndPoint(IPAddress.Any, gatewayPort);
|
options.GatewayListeningEndpoint = new IPEndPoint(IPAddress.Any, gatewayPort);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
siloBuilder
|
siloBuilder
|
||||||
.Configure<MessagingOptions>(options =>
|
.Configure<MessagingOptions>(options =>
|
||||||
{
|
{
|
||||||
@@ -221,7 +228,7 @@ public static class ApiBootstrap
|
|||||||
options.ProbeTimeout = TimeSpan.FromSeconds(10);
|
options.ProbeTimeout = TimeSpan.FromSeconds(10);
|
||||||
options.IAmAliveTablePublishTimeout = TimeSpan.FromSeconds(30);
|
options.IAmAliveTablePublishTimeout = TimeSpan.FromSeconds(30);
|
||||||
options.MaxJoinAttemptTime = TimeSpan.FromSeconds(120);
|
options.MaxJoinAttemptTime = TimeSpan.FromSeconds(120);
|
||||||
|
|
||||||
// Improved settings for development environments with stale members
|
// Improved settings for development environments with stale members
|
||||||
options.DefunctSiloCleanupPeriod = TimeSpan.FromMinutes(1);
|
options.DefunctSiloCleanupPeriod = TimeSpan.FromMinutes(1);
|
||||||
options.DefunctSiloExpiration = TimeSpan.FromMinutes(2);
|
options.DefunctSiloExpiration = TimeSpan.FromMinutes(2);
|
||||||
@@ -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>();
|
||||||
@@ -357,7 +374,7 @@ public static class ApiBootstrap
|
|||||||
|
|
||||||
services.AddSingleton<IMessengerService, MessengerService>();
|
services.AddSingleton<IMessengerService, MessengerService>();
|
||||||
services.AddSingleton<IDiscordService, DiscordService>();
|
services.AddSingleton<IDiscordService, DiscordService>();
|
||||||
|
|
||||||
// Admin services
|
// Admin services
|
||||||
services.AddSingleton<IAdminConfigurationService, AdminConfigurationService>();
|
services.AddSingleton<IAdminConfigurationService, AdminConfigurationService>();
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Managing.Common
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Common
|
||||||
{
|
{
|
||||||
public class Constants
|
public class Constants
|
||||||
{
|
{
|
||||||
@@ -65,21 +67,41 @@
|
|||||||
{
|
{
|
||||||
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
|
||||||
{
|
{
|
||||||
public const int USD = 30;
|
public const int USD = 30;
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ 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;
|
||||||
}
|
}
|
||||||
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 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; }
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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()
|
.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)
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,27 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Platforms>AnyCPU;x64</Platforms>
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="9.9.7" />
|
<PackageReference Include="Binance.Net" Version="9.9.7"/>
|
||||||
<PackageReference Include="CryptoExchange.Net" Version="7.5.2" />
|
<PackageReference Include="CryptoExchange.Net" Version="7.5.2"/>
|
||||||
<PackageReference Include="FTX.Net" Version="1.0.16" />
|
<PackageReference Include="FTX.Net" Version="1.0.16"/>
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="4.6.5" />
|
<PackageReference Include="KrakenExchange.Net" Version="4.6.5"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj" />
|
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj"/>
|
||||||
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj" />
|
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Helpers\"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public static class PriceExtensions
|
|||||||
{
|
{
|
||||||
public static List<Candle> GetCandles(this Round[] prices, Timeframe timeframe, Ticker ticker)
|
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>
|
var CHART_PERIODS = new Dictionary<Timeframe, int>
|
||||||
{
|
{
|
||||||
{ Timeframe.FiveMinutes, 60 * 5 },
|
{ Timeframe.FiveMinutes, 60 * 5 },
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -101,4 +104,4 @@ public class Gbc : ISubgraphPrices
|
|||||||
|
|
||||||
return Task.FromResult(tickers.AsEnumerable());
|
return Task.FromResult(tickers.AsEnumerable());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import React, {useState} from 'react'
|
|||||||
import {Card, FormInput, GridTile} from '../../components/mollecules'
|
import {Card, FormInput, GridTile} from '../../components/mollecules'
|
||||||
import useApiUrlStore from '../../app/store/apiStore'
|
import useApiUrlStore from '../../app/store/apiStore'
|
||||||
import {
|
import {
|
||||||
type AgentBalanceHistory,
|
type AgentBalanceHistory,
|
||||||
BotStatus,
|
BotStatus,
|
||||||
DataClient,
|
DataClient,
|
||||||
type Position,
|
type Position,
|
||||||
TradeDirection,
|
TradeDirection,
|
||||||
type UserStrategyDetailsViewModel
|
type UserStrategyDetailsViewModel
|
||||||
} from '../../generated/ManagingApi'
|
} from '../../generated/ManagingApi'
|
||||||
|
|
||||||
interface AgentData {
|
interface AgentData {
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -164,6 +181,44 @@ const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
|
|||||||
showTotal={showTotal}
|
showTotal={showTotal}
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user