From 029ba5f40ec232ff561ea3f03159a7f56c110170 Mon Sep 17 00:00:00 2001 From: Oda <102867384+CryptoOda@users.noreply.github.com> Date: Fri, 19 Jul 2024 08:31:09 +0700 Subject: [PATCH] Add funding rate watcher (#2) * Add FundingRate interfaces and worker * Add build on PR * Remove zip * Specify the solution path * Add build for worker too * Set up StatisticService.cs for funding rate * Add Fundingrate alerts * Send alert when big funding rate change + add SlashCommands.cs for fundingrate * Remove fixtures * Refact names * Renames --- .github/workflows/caprover.yml | 32 +-- .github/workflows/dotnet.yml | 30 +++ assets/documentation/DeltaNeutralWorker.md | 29 +++ src/Managing.Api.Workers/Program.cs | 28 +-- .../Workers/FundingRatesWatcher.cs | 28 +++ src/Managing.Api/appsettings.Oda-Sandbox.json | 1 + src/Managing.Api/appsettings.json | 5 +- .../Repositories/IEvmManager.cs | 2 + .../Repositories/IStatisticRepository.cs | 6 +- .../Services/IDiscordService.cs | 9 +- .../Services/IExchangeService.cs | 24 ++- .../Services/IMessengerService.cs | 9 +- .../Services/ITradaoService.cs | 3 +- .../Services/ITradingService.cs | 1 + .../Abstractions/IStatisticService.cs | 4 +- .../BaseWorker.cs | 15 +- .../StatisticService.cs | 82 ++++++-- .../Shared/MessengerService.cs | 20 +- .../Trading/TradingService.cs | 7 +- src/Managing.Common/Constants.cs | 3 +- src/Managing.Common/Enums.cs | 54 ++--- src/Managing.Domain/Statistics/FundingRate.cs | 13 ++ .../MongoDb/Collections/FundingRateDto.cs | 15 ++ .../MongoDb/MongoMappers.cs | 110 ++++++++-- .../StatisticRepository.cs | 35 +++- .../Abstractions/IExchangeProcessor.cs | 2 + .../ExchangeService.cs | 17 +- .../Exchanges/BaseProcessor.cs | 2 + .../Exchanges/BinanceProcessor.cs | 6 + .../Exchanges/EvmProcessor.cs | 6 + .../Exchanges/FtxProcessor.cs | 6 + .../Exchanges/KrakenProcessor.cs | 6 + .../Helpers/BinanceHelpers.cs | 24 +-- .../Helpers/FtxHelpers.cs | 64 +----- .../Discord/DiscordHelpers.cs | 83 +++++++- .../Discord/DiscordService.cs | 198 +++++++++++------- .../Discord/DiscordSettings.cs | 5 +- .../Discord/SlashCommands.cs | 14 +- .../TradaoTests.cs | 46 +++- .../EvmManager.cs | 40 ++-- .../Services/TradaoService.cs | 134 +++++++++++- 41 files changed, 914 insertions(+), 304 deletions(-) create mode 100644 .github/workflows/dotnet.yml create mode 100644 assets/documentation/DeltaNeutralWorker.md create mode 100644 src/Managing.Api.Workers/Workers/FundingRatesWatcher.cs create mode 100644 src/Managing.Domain/Statistics/FundingRate.cs create mode 100644 src/Managing.Infrastructure.Database/MongoDb/Collections/FundingRateDto.cs diff --git a/.github/workflows/caprover.yml b/.github/workflows/caprover.yml index ec60dc1..243361b 100644 --- a/.github/workflows/caprover.yml +++ b/.github/workflows/caprover.yml @@ -17,19 +17,19 @@ jobs: # npm install # npm run build # npm run test - - name: Create deploy.tar - uses: a7ul/tar-action@v1.1.0 - with: - command: c - cwd: "./" - files: | - scripts/build_and_run.sh - captain-definition - outPath: deploy.tar - - name: Deploy App to CapRover - uses: caprover/deploy-from-github@v1.0.1 - with: - server: '${{ secrets.CAPROVER_SERVER }}' - app: '${{ secrets.APP_NAME }}' - token: '${{ secrets.MANAGING_APPS }}' - +# - name: Create deploy.tar +# uses: a7ul/tar-action@v1.1.0 +# with: +# command: c +# cwd: "./" +# files: | +# scripts/build_and_run.sh +# captain-definition +# outPath: deploy.tar +# - name: Deploy App to CapRover +# uses: caprover/deploy-from-github@v1.0.1 +# with: +# server: '${{ secrets.CAPROVER_SERVER }}' +# app: '${{ secrets.APP_NAME }}' +# token: '${{ secrets.MANAGING_APPS }}' +# diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..91d7173 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,30 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + push: + branches: [ "dev" ] + pull_request: + branches: [ "dev" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Restore API dependencies + run: dotnet restore ./src/Managing.Api/Managing.Api.csproj + - name: Build API + run: dotnet build --no-restore ./src/Managing.Api/Managing.Api.csproj + - name: Restore Worker dependencies + run: dotnet restore ./src/Managing.Api.Workers/Managing.Api.Workers.csproj + - name: Build Worker + run: dotnet build --no-restore ./src/Managing.Api.Workers/Managing.Api.Workers.csproj diff --git a/assets/documentation/DeltaNeutralWorker.md b/assets/documentation/DeltaNeutralWorker.md new file mode 100644 index 0000000..bec1612 --- /dev/null +++ b/assets/documentation/DeltaNeutralWorker.md @@ -0,0 +1,29 @@ +```mermaid +sequenceDiagram + participant User + participant DeltaNeutralWatcher as Delta Neutral Watcher + participant GMX as GMX Exchange + participant Hyperliquid as Hyperliquid Exchange + participant Database + participant TradeBot as Trade Bot + + User->>DeltaNeutralWatcher: Start Bot + + loop Watcher + DeltaNeutralWatcher->>GMX: Request Market Data + DeltaNeutralWatcher->>Hyperliquid: Request Market Data + GMX-->>DeltaNeutralWatcher: Provide Market Data + Hyperliquid-->>DeltaNeutralWatcher: Provide Market Data + DeltaNeutralWatcher->>DeltaNeutralWatcher: Check Delta Neutral Opportunities + DeltaNeutralWatcher->>Database: Save Opportunities + end + + loop Bot + TradeBot->>Database: Fetch Opportunities + TradeBot->>GMX: Send Orders for Long Position + TradeBot->>Hyperliquid: Send Orders for Short Position + GMX-->>TradeBot: Execution Report + Hyperliquid-->>TradeBot: Execution Report + TradeBot->>Database: Fetch for Better Opportunities + end +``` \ No newline at end of file diff --git a/src/Managing.Api.Workers/Program.cs b/src/Managing.Api.Workers/Program.cs index 80d871e..a941830 100644 --- a/src/Managing.Api.Workers/Program.cs +++ b/src/Managing.Api.Workers/Program.cs @@ -16,7 +16,7 @@ using Serilog.Sinks.Elasticsearch; var builder = WebApplication.CreateBuilder(args); builder.Configuration.SetBasePath(System.AppContext.BaseDirectory); builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json"); + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json"); builder.Host.UseSerilog((hostBuilder, loggerConfiguration) => { @@ -49,12 +49,12 @@ builder.Services.AddControllers().AddJsonOptions(options => builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder => { builder - .SetIsOriginAllowed((host) => true) - .AllowAnyOrigin() - .WithOrigins("http://localhost:3000/") - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials(); + .SetIsOriginAllowed((host) => true) + .AllowAnyOrigin() + .WithOrigins("http://localhost:3000/") + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); })); builder.Services.AddSignalR().AddJsonProtocol(); @@ -85,15 +85,18 @@ builder.Services.AddSwaggerGen(options => Scheme = "Bearer", BearerFormat = "JWT" }); - options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement{ + options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement + { { - new Microsoft.OpenApi.Models.OpenApiSecurityScheme{ - Reference = new Microsoft.OpenApi.Models.OpenApiReference{ + new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Reference = new Microsoft.OpenApi.Models.OpenApiReference + { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "Bearer" } }, - new string[]{} + new string[] { } } }); }); @@ -112,6 +115,7 @@ builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); +builder.Services.AddHostedService(); // App var app = builder.Build(); @@ -145,4 +149,4 @@ app.UseEndpoints(endpoints => endpoints.MapHub("/positionhub"); }); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/src/Managing.Api.Workers/Workers/FundingRatesWatcher.cs b/src/Managing.Api.Workers/Workers/FundingRatesWatcher.cs new file mode 100644 index 0000000..d3c0378 --- /dev/null +++ b/src/Managing.Api.Workers/Workers/FundingRatesWatcher.cs @@ -0,0 +1,28 @@ +using Managing.Application.Workers; +using Managing.Application.Workers.Abstractions; +using Managing.Common; + +namespace Managing.Api.Workers.Workers; + +public class FundingRatesWatcher : BaseWorker +{ + private readonly IStatisticService _statisticService; + + public FundingRatesWatcher( + ILogger logger, + IStatisticService statisticService, + IWorkerService workerService) : base( + Enums.WorkerType.FundingRatesWatcher, + logger, + TimeSpan.FromMinutes(30), + workerService + ) + { + _statisticService = statisticService; + } + + protected override async Task Run(CancellationToken cancellationToken) + { + await _statisticService.UpdateFundingRates(); + } +} \ No newline at end of file diff --git a/src/Managing.Api/appsettings.Oda-Sandbox.json b/src/Managing.Api/appsettings.Oda-Sandbox.json index c8c284b..92df16f 100644 --- a/src/Managing.Api/appsettings.Oda-Sandbox.json +++ b/src/Managing.Api/appsettings.Oda-Sandbox.json @@ -30,6 +30,7 @@ "TradesChannelId": 998374177763491851, "TroublesChannelId": 1015761955321040917, "CopyTradingChannelId": 1132022887012909126, + "FundingRateChannelId": 1263566138709774336, "RequestsChannelId": 1018589494968078356, "ButtonExpirationMinutes": 10 }, diff --git a/src/Managing.Api/appsettings.json b/src/Managing.Api/appsettings.json index 09b5c39..b7f3ec1 100644 --- a/src/Managing.Api/appsettings.json +++ b/src/Managing.Api/appsettings.json @@ -2,7 +2,9 @@ "Authentication": { "Schemes": { "Bearer": { - "ValidAudiences": [ "http://localhost:3000/" ], + "ValidAudiences": [ + "http://localhost:3000/" + ], "ValidIssuer": "Managing" } } @@ -42,6 +44,7 @@ "TroublesChannelId": 1015761955321040917, "CopyTradingChannelId": 1132022887012909126, "RequestsChannelId": 1018589494968078356, + "FundingRateChannelId": 1263566138709774336, "ButtonExpirationMinutes": 10 }, "AllowedHosts": "*" diff --git a/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs b/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs index fb50b97..88a54e1 100644 --- a/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs +++ b/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs @@ -1,6 +1,7 @@ using Managing.Domain.Accounts; using Managing.Domain.Candles; using Managing.Domain.Evm; +using Managing.Domain.Statistics; using Managing.Domain.Trades; using static Managing.Common.Enums; @@ -33,4 +34,5 @@ public interface IEvmManager Task GetFee(string chainName); Task> GetOrders(Account account, Ticker ticker); Task GetTrade(string reference, string arbitrum, Ticker ticker); + Task> GetFundingRates(); } diff --git a/src/Managing.Application.Abstractions/Repositories/IStatisticRepository.cs b/src/Managing.Application.Abstractions/Repositories/IStatisticRepository.cs index 3b583fb..da79729 100644 --- a/src/Managing.Application.Abstractions/Repositories/IStatisticRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/IStatisticRepository.cs @@ -17,4 +17,8 @@ public interface IStatisticRepository void UpdateBadTrader(Trader trader); Task InsertBadTrader(Trader trader); Task RemoveBadTrader(Trader trader); -} + List GetFundingRates(); + Task RemoveFundingRate(FundingRate oldRate); + Task InsertFundingRate(FundingRate newRate); + void UpdateFundingRate(FundingRate oldRate, FundingRate newRate); +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/IDiscordService.cs b/src/Managing.Application.Abstractions/Services/IDiscordService.cs index e5d1a3a..47d5eab 100644 --- a/src/Managing.Application.Abstractions/Services/IDiscordService.cs +++ b/src/Managing.Application.Abstractions/Services/IDiscordService.cs @@ -6,7 +6,9 @@ namespace Managing.Application.Abstractions.Services; public interface IDiscordService { - Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, Timeframe timeframe); + Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, + Timeframe timeframe); + Task SendPosition(Position position); Task SendClosingPosition(Position position); Task SendMessage(string v); @@ -16,4 +18,7 @@ public interface IDiscordService Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount); Task SendBestTraders(List traders); Task SendBadTraders(List traders); -} + Task SendDowngradedFundingRate(FundingRate oldRate); + Task SendNewTopFundingRate(FundingRate newRate); + Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate); +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/IExchangeService.cs b/src/Managing.Application.Abstractions/Services/IExchangeService.cs index 9759a06..198f95f 100644 --- a/src/Managing.Application.Abstractions/Services/IExchangeService.cs +++ b/src/Managing.Application.Abstractions/Services/IExchangeService.cs @@ -1,7 +1,8 @@ -using Managing.Domain.Trades; +using Managing.Domain.Accounts; using Managing.Domain.Candles; +using Managing.Domain.Statistics; +using Managing.Domain.Trades; using static Managing.Common.Enums; -using Managing.Domain.Accounts; namespace Managing.Application.Abstractions.Services; @@ -19,16 +20,22 @@ public interface IExchangeService bool isForPaperTrading = false, DateTime? currentDate = null, bool ioc = true); + Task GetBalance(Account account, bool isForPaperTrading = false); Task> GetBalances(Account account, bool isForPaperTrading = false); decimal GetPrice(Account account, Ticker ticker, DateTime date); Task GetTrade(Account account, string order, Ticker ticker); Task> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe interval); + Task OpenStopLoss(Account account, Ticker ticker, TradeDirection originalDirection, decimal stopLossPrice, decimal quantity, bool isForPaperTrading = false, DateTime? currentDate = null); + Task> GetTickers(Account account, Timeframe timeframe); - Task OpenTakeProfit(Account account, Ticker ticker, TradeDirection originalDirection, decimal takeProfitPrice, + + Task OpenTakeProfit(Account account, Ticker ticker, TradeDirection originalDirection, + decimal takeProfitPrice, decimal quantity, bool isForPaperTrading = false, DateTime? currentDate = null); + Task ClosePosition(Account account, Position position, decimal lastPrice, bool isForPaperTrading = false); decimal GetVolume(Account account, Ticker ticker); Task> GetTrades(Account account, Ticker ticker); @@ -36,10 +43,17 @@ public interface IExchangeService decimal GetFee(Account account, bool isForPaperTrading = false); Candle GetCandle(Account account, Ticker ticker, DateTime date); Task GetQuantityInPosition(Account account, Ticker ticker); - Task> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate, Timeframe timeframe); + + Task> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate, + Timeframe timeframe); + decimal GetBestPrice(Account account, Ticker ticker, decimal lastPrice, decimal quantity, TradeDirection direction); Orderbook GetOrderbook(Account account, Ticker ticker); - Trade BuildEmptyTrade(Ticker ticker, decimal price, decimal quantity, TradeDirection direction, decimal? leverage, TradeType tradeType, DateTime dateTime, TradeStatus tradeStatus = TradeStatus.PendingOpen); + + Trade BuildEmptyTrade(Ticker ticker, decimal price, decimal quantity, TradeDirection direction, decimal? leverage, + TradeType tradeType, DateTime dateTime, TradeStatus tradeStatus = TradeStatus.PendingOpen); + Task> GetOpenOrders(Account account, Ticker ticker); Task GetTrade(string reference, string orderId, Ticker ticker); + Task> GetFundingRates(); } \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/IMessengerService.cs b/src/Managing.Application.Abstractions/Services/IMessengerService.cs index 0d7817b..0f88e14 100644 --- a/src/Managing.Application.Abstractions/Services/IMessengerService.cs +++ b/src/Managing.Application.Abstractions/Services/IMessengerService.cs @@ -6,7 +6,9 @@ namespace Managing.Application.Abstractions.Services; public interface IMessengerService { - Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, Timeframe timeframe); + Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, + Timeframe timeframe); + Task SendPosition(Position position); Task SendClosingPosition(Position position); Task SendMessage(string v); @@ -16,4 +18,7 @@ public interface IMessengerService Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount); Task SendBestTraders(List traders); Task SendBadTraders(List filteredTrader); -} + Task SendDowngradedFundingRate(FundingRate oldRate); + Task SendNewTopFundingRate(FundingRate newRate); + Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate); +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/ITradaoService.cs b/src/Managing.Application.Abstractions/Services/ITradaoService.cs index af6b2ef..ad97757 100644 --- a/src/Managing.Application.Abstractions/Services/ITradaoService.cs +++ b/src/Managing.Application.Abstractions/Services/ITradaoService.cs @@ -8,4 +8,5 @@ public interface ITradaoService Task> GetBadTrader(); Task> GetBestTrader(); Task> GetTrades(string address); -} + Task> GetFundingRates(); +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/ITradingService.cs b/src/Managing.Application.Abstractions/Services/ITradingService.cs index df7cc03..b8472af 100644 --- a/src/Managing.Application.Abstractions/Services/ITradingService.cs +++ b/src/Managing.Application.Abstractions/Services/ITradingService.cs @@ -32,4 +32,5 @@ public interface ITradingService decimal GetFee(Account account, bool isForPaperTrading = false); Task WatchTrader(); IEnumerable GetTradersWatch(); + void UpdateDeltaNeutralOpportunities(); } \ No newline at end of file diff --git a/src/Managing.Application.Workers/Abstractions/IStatisticService.cs b/src/Managing.Application.Workers/Abstractions/IStatisticService.cs index 8ab8d08..56ad93a 100644 --- a/src/Managing.Application.Workers/Abstractions/IStatisticService.cs +++ b/src/Managing.Application.Workers/Abstractions/IStatisticService.cs @@ -16,4 +16,6 @@ public interface IStatisticService Task UpdateNoobiesboard(); Task UpdateSpotlight(); Task UpdateTopVolumeTicker(Enums.TradingExchanges exchange, int top); -} + Task UpdateFundingRates(); + Task> GetFundingRates(); +} \ No newline at end of file diff --git a/src/Managing.Application.Workers/BaseWorker.cs b/src/Managing.Application.Workers/BaseWorker.cs index 04856ff..c1eeacc 100644 --- a/src/Managing.Application.Workers/BaseWorker.cs +++ b/src/Managing.Application.Workers/BaseWorker.cs @@ -1,5 +1,4 @@ - -using Managing.Application.Workers.Abstractions; +using Managing.Application.Workers.Abstractions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -40,7 +39,8 @@ public abstract class BaseWorker : BackgroundService where T : class } else { - _logger.LogInformation($"[{_workerType}] Last run : {worker.LastRunTime} - Execution Count : {worker.ExecutionCount}"); + _logger.LogInformation( + $"[{_workerType}] Last run : {worker.LastRunTime} - Execution Count : {worker.ExecutionCount}"); _executionCount = worker.ExecutionCount; } @@ -49,8 +49,7 @@ public abstract class BaseWorker : BackgroundService where T : class while (!cancellationToken.IsCancellationRequested) { worker = await _workerService.GetWorker(_workerType); - - //if (true) + if (worker.IsActive) { await Run(cancellationToken); @@ -60,11 +59,13 @@ public abstract class BaseWorker : BackgroundService where T : class } else { - _logger.LogInformation($"[{_workerType}] Worker not active. Next run at : {DateTime.UtcNow.Add(_delay)}"); + _logger.LogInformation( + $"[{_workerType}] Worker not active. Next run at : {DateTime.UtcNow.Add(_delay)}"); } await Task.Delay(_delay); } + _logger.LogInformation($"[{_workerType}] Stopped"); } catch (Exception ex) @@ -74,4 +75,4 @@ public abstract class BaseWorker : BackgroundService where T : class } protected abstract Task Run(CancellationToken cancellationToken); -} +} \ No newline at end of file diff --git a/src/Managing.Application.Workers/StatisticService.cs b/src/Managing.Application.Workers/StatisticService.cs index 6600beb..b42b34c 100644 --- a/src/Managing.Application.Workers/StatisticService.cs +++ b/src/Managing.Application.Workers/StatisticService.cs @@ -90,6 +90,63 @@ public class StatisticService : IStatisticService } } + public async Task UpdateFundingRates() + { + // Get fundingRate from database + var previousFundingRate = await GetFundingRates(); + // var fundingRates = await .GetFundingRates(); + + var newFundingRates = await _tradaoService.GetFundingRates(); + var topRates = newFundingRates + .OrderByDescending(fr => fr.Rate) + .Take(3) + .ToList(); + + // Old position not in the new top + foreach (var oldRate in previousFundingRate) + { + if (topRates.All(tr => !SameFundingRate(tr, oldRate))) + { + // Close position + await _messengerService.SendDowngradedFundingRate(oldRate); + await _statisticRepository.RemoveFundingRate(oldRate); + } + } + + // New position not in the old top + foreach (var newRate in topRates) + { + if (previousFundingRate.All(tr => !SameFundingRate(tr, newRate))) + { + // Open position + await _messengerService.SendNewTopFundingRate(newRate); + await _statisticRepository.InsertFundingRate(newRate); + } + else if (previousFundingRate.Any(tr => SameFundingRate(tr, newRate))) + { + var oldRate = previousFundingRate.FirstOrDefault(tr => SameFundingRate(tr, newRate)); + if (oldRate != null && Math.Abs(oldRate.Rate - newRate.Rate) > 1m) + { + await _messengerService.SendFundingRateUpdate(oldRate, newRate); + _statisticRepository.UpdateFundingRate(oldRate, newRate); + } + } + } + } + + private bool SameFundingRate(FundingRate oldRate, FundingRate newRate) + { + return oldRate.Ticker == newRate.Ticker && + oldRate.Exchange == newRate.Exchange && + oldRate.Direction == newRate.Direction; + } + + public Task> GetFundingRates() + { + var previousFundingRate = _statisticRepository.GetFundingRates(); + return Task.FromResult(previousFundingRate); + } + public IList GetLastTopVolumeTicker() { var from = DateTime.UtcNow.AddDays(-1); @@ -113,16 +170,17 @@ public class StatisticService : IStatisticService if (overview != null) { - if(overview.Spotlights.Count < overview.ScenarioCount) + if (overview.Spotlights.Count < overview.ScenarioCount) { - _logger.LogInformation($"Spotlights not up to date. {overview.Spotlights.Count}/{overview.ScenarioCount}"); + _logger.LogInformation( + $"Spotlights not up to date. {overview.Spotlights.Count}/{overview.ScenarioCount}"); } else { _logger.LogInformation("No need to update spotlights"); return; } - } + } else { overview = new SpotlightOverview @@ -188,14 +246,14 @@ public class StatisticService : IStatisticService }; var backtest = _backtester.RunScalpingBotBacktest( - account, - moneyManagement, - ticker, - scenario, - timeframe, - CandleExtensions.GetMinimalDays(timeframe), - 1000, - isForWatchingOnly: true); + account, + moneyManagement, + ticker, + scenario, + timeframe, + CandleExtensions.GetMinimalDays(timeframe), + 1000, + isForWatchingOnly: true); return backtest.Signals; } @@ -301,4 +359,4 @@ public class StatisticService : IStatisticService await _messengerService.SendBadTraders(lastBadTrader); } -} +} \ No newline at end of file diff --git a/src/Managing.Application/Shared/MessengerService.cs b/src/Managing.Application/Shared/MessengerService.cs index 92ab997..8d1843d 100644 --- a/src/Managing.Application/Shared/MessengerService.cs +++ b/src/Managing.Application/Shared/MessengerService.cs @@ -44,7 +44,8 @@ public class MessengerService : IMessengerService await _discordService.SendPosition(position); } - public async Task SendSignal(string message, Enums.TradingExchanges exchange, Enums.Ticker ticker, Enums.TradeDirection direction, Enums.Timeframe timeframe) + public async Task SendSignal(string message, Enums.TradingExchanges exchange, Enums.Ticker ticker, + Enums.TradeDirection direction, Enums.Timeframe timeframe) { await _discordService.SendSignal(message, exchange, ticker, direction, timeframe); } @@ -63,4 +64,19 @@ public class MessengerService : IMessengerService { await _discordService.SendBadTraders(traders); } -} + + public async Task SendDowngradedFundingRate(FundingRate oldRate) + { + await _discordService.SendDowngradedFundingRate(oldRate); + } + + public async Task SendNewTopFundingRate(FundingRate newRate) + { + await _discordService.SendNewTopFundingRate(newRate); + } + + public async Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate) + { + await _discordService.SendFundingRateUpdate(oldRate, newRate); + } +} \ No newline at end of file diff --git a/src/Managing.Application/Trading/TradingService.cs b/src/Managing.Application/Trading/TradingService.cs index 12f7263..002c290 100644 --- a/src/Managing.Application/Trading/TradingService.cs +++ b/src/Managing.Application/Trading/TradingService.cs @@ -3,10 +3,10 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Domain.Accounts; using Managing.Domain.Scenarios; +using Managing.Domain.Shared.Helpers; using Managing.Domain.Statistics; using Managing.Domain.Strategies; using Managing.Domain.Trades; -using Managing.Domain.Shared.Helpers; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -260,6 +260,11 @@ public class TradingService : ITradingService return watchAccount; } + public void UpdateDeltaNeutralOpportunities() + { + var fundingRates = _exchangeService.GetFundingRates(); + } + private async Task ManageTrader(TraderFollowup a, List tickers) { var shortAddress = a.Account.Address.Substring(0, 6); diff --git a/src/Managing.Common/Constants.cs b/src/Managing.Common/Constants.cs index a91688a..b390e62 100644 --- a/src/Managing.Common/Constants.cs +++ b/src/Managing.Common/Constants.cs @@ -7,6 +7,7 @@ public const string Leaderboard = "leaderboard"; public const string Noobiesboard = "noobiesboard"; public const string LeaderboardPosition = "leaderboardposition"; + public const string FundingRates = "fundingrates"; } public class DiscordButtonAction @@ -36,4 +37,4 @@ public const string Usdt = "USDT"; } } -} +} \ No newline at end of file diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index b342b8a..9850349 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -113,54 +113,67 @@ public static class Enums // Summary: // Limit order Limit = 0, + // // Summary: // Symbol order Market = 1, + // // Summary: // Stop market order StopMarket = 2, + // // Summary: // Stop limit order StopLimit = 3, + // // Summary: // Stop loss order StopLoss = 4, + // // Summary: // Take profit order TakeProfit = 5, + // // Summary: // Stop loss profit order StopLossProfit = 6, + // // Summary: // Stop loss profit limit order StopLossProfitLimit = 7, + // // Summary: // Stop loss limit order StopLossLimit = 8, + // // Summary: // Take profit limit order TakeProfitLimit = 9, + // // Summary: // Trailing stop order TrailingStop = 10, + // // Summary: // Trailing stop limit order TrailingStopLimit = 11, + // // Summary: // Stop loss and limit order StopLossAndLimit = 12, + // // Summary: // Settle position @@ -201,22 +214,27 @@ public static class Enums /// 5m /// FiveMinutes, + /// /// 15m /// FifteenMinutes, + /// /// 30m /// ThirtyMinutes, + /// /// 1h /// OneHour, + /// /// 4h /// FourHour, + /// /// 1d /// @@ -225,25 +243,20 @@ public static class Enums public enum Ticker { + AAVE, ADA, APE, - ALICE, ALGO, + ARB, ATOM, AVAX, - AXS, - BAT, BNB, BTC, BAL, - C98, - CHR, CHZ, COMP, CRO, CRV, - CVC, - DEFI, DOGE, DOT, DYDX, @@ -254,29 +267,22 @@ public static class Enums FLM, FTM, GALA, - GMT, GMX, GRT, - HNT, IMX, JASMY, - KAVA, KSM, LDO, LINK, - LOOKS, LRC, LTC, MANA, MATIC, MKR, NEAR, - NEO, - OMG, - ONE, - ONT, + OP, + PEPE, QTUM, - REEF, REN, ROSE, RSR, @@ -284,23 +290,17 @@ public static class Enums SAND, SOL, SRM, - STMX, SUSHI, - SXP, THETA, UNI, USDC, USDT, - VET, - WAVES, + WIF, XMR, XRP, XTZ, - YFI, - ZEC, - ZIL } - + public enum WorkerType { PriceOneMinute, @@ -318,7 +318,8 @@ public static class Enums TraderWatcher, LeaderboardWorker, Noobiesboard, - BotManager + BotManager, + FundingRatesWatcher } public enum WorkflowUsage @@ -341,5 +342,4 @@ public static class Enums Position, MoneyManagement } - -} +} \ No newline at end of file diff --git a/src/Managing.Domain/Statistics/FundingRate.cs b/src/Managing.Domain/Statistics/FundingRate.cs new file mode 100644 index 0000000..01eb5ff --- /dev/null +++ b/src/Managing.Domain/Statistics/FundingRate.cs @@ -0,0 +1,13 @@ +using Managing.Common; + +namespace Managing.Domain.Statistics; + +public class FundingRate +{ + public Enums.Ticker Ticker { get; set; } + public Enums.TradingExchanges Exchange { get; set; } + public decimal Rate { get; set; } + public decimal OpenInterest { get; set; } + public DateTime Date { get; set; } + public Enums.TradeDirection Direction { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/Collections/FundingRateDto.cs b/src/Managing.Infrastructure.Database/MongoDb/Collections/FundingRateDto.cs new file mode 100644 index 0000000..df5c773 --- /dev/null +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/FundingRateDto.cs @@ -0,0 +1,15 @@ +using Managing.Common; +using Managing.Infrastructure.Databases.MongoDb.Attributes; +using Managing.Infrastructure.Databases.MongoDb.Configurations; + +namespace Managing.Infrastructure.Databases.MongoDb.Collections; + +[BsonCollection("FundingRates")] +public class FundingRateDto : Document +{ + public Enums.Ticker Ticker { get; set; } + public decimal Rate { get; set; } + public Enums.TradingExchanges Exchange { get; set; } + public DateTime Date { get; set; } + public Enums.TradeDirection Direction { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs index e1c0592..0e67f03 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs @@ -16,7 +16,8 @@ namespace Managing.Infrastructure.Databases.MongoDb; public static class MongoMappers { - #region Statistics + #region Statistics + internal static TopVolumeTickerDto Map(TopVolumeTicker topVolumeTicker) { return new TopVolumeTickerDto @@ -44,6 +45,7 @@ public static class MongoMappers #endregion #region Accounts + internal static AccountDto Map(Account request) { return new AccountDto @@ -84,9 +86,11 @@ public static class MongoMappers return a; } + #endregion #region Workers + internal static WorkerDto Map(Worker worker) { return new WorkerDto @@ -118,6 +122,7 @@ public static class MongoMappers #endregion #region Backtests + internal static Backtest Map(BacktestDto b) { return new Backtest( @@ -165,7 +170,8 @@ public static class MongoMappers #endregion - #region Candles + #region Candles + public static Candle Map(CandleDto candle) { if (candle == null) @@ -213,10 +219,10 @@ public static class MongoMappers return candles.ConvertAll(candle => Map(candle)); } - #endregion #region Positions + public static PositionDto Map(Position position) { var p = new PositionDto @@ -267,9 +273,13 @@ public static class MongoMappers public static Position Map(PositionDto dto) { - var position = new Position(dto.AccountName, originDirection: dto.OriginDirection, dto.Ticker, Map(dto.MoneyManagement), dto.Initiator, dto.Date) + var position = new Position(dto.AccountName, originDirection: dto.OriginDirection, dto.Ticker, + Map(dto.MoneyManagement), dto.Initiator, dto.Date) { - Open = new Trade(date: dto.Open.Date, direction: dto.Open.Direction, status: dto.Open.Status, tradeType: dto.Open.TradeType, ticker: dto.Open.Ticker, quantity: dto.Open.Quantity, price: dto.Open.Price, leverage: dto.Open.Leverage, exchangeOrderId: dto.Open.ExchangeOrderId, message: dto.Open.Message), + Open = new Trade(date: dto.Open.Date, direction: dto.Open.Direction, status: dto.Open.Status, + tradeType: dto.Open.TradeType, ticker: dto.Open.Ticker, quantity: dto.Open.Quantity, + price: dto.Open.Price, leverage: dto.Open.Leverage, exchangeOrderId: dto.Open.ExchangeOrderId, + message: dto.Open.Message), ProfitAndLoss = new ProfitAndLoss { Realized = dto.ProfitAndLoss }, Status = dto.Status, SignalIdentifier = dto.SignalIdentifier, @@ -278,17 +288,26 @@ public static class MongoMappers if (dto.StopLoss != null) { - position.StopLoss = new Trade(date: dto.StopLoss.Date, direction: dto.StopLoss.Direction, status: dto.StopLoss.Status, tradeType: dto.StopLoss.TradeType, ticker: dto.StopLoss.Ticker, quantity: dto.StopLoss.Quantity, price: dto.StopLoss.Price, leverage: dto.StopLoss.Leverage, exchangeOrderId: dto.StopLoss.ExchangeOrderId, message: dto.StopLoss.Message); + position.StopLoss = new Trade(date: dto.StopLoss.Date, direction: dto.StopLoss.Direction, + status: dto.StopLoss.Status, tradeType: dto.StopLoss.TradeType, ticker: dto.StopLoss.Ticker, + quantity: dto.StopLoss.Quantity, price: dto.StopLoss.Price, leverage: dto.StopLoss.Leverage, + exchangeOrderId: dto.StopLoss.ExchangeOrderId, message: dto.StopLoss.Message); } if (dto.TakeProfit1 != null) { - position.TakeProfit1 = new Trade(date: dto.TakeProfit1.Date, direction: dto.TakeProfit1.Direction, status: dto.TakeProfit1.Status, tradeType: dto.TakeProfit1.TradeType, ticker: dto.TakeProfit1.Ticker, quantity: dto.TakeProfit1.Quantity, price: dto.TakeProfit1.Price, leverage: dto.TakeProfit1.Leverage, exchangeOrderId: dto.TakeProfit1.ExchangeOrderId, message: dto.TakeProfit1.Message); + position.TakeProfit1 = new Trade(date: dto.TakeProfit1.Date, direction: dto.TakeProfit1.Direction, + status: dto.TakeProfit1.Status, tradeType: dto.TakeProfit1.TradeType, ticker: dto.TakeProfit1.Ticker, + quantity: dto.TakeProfit1.Quantity, price: dto.TakeProfit1.Price, leverage: dto.TakeProfit1.Leverage, + exchangeOrderId: dto.TakeProfit1.ExchangeOrderId, message: dto.TakeProfit1.Message); } if (dto.TakeProfit2 != null) { - position.TakeProfit2 = new Trade(date: dto.TakeProfit2.Date, direction: dto.TakeProfit2.Direction, status: dto.TakeProfit2.Status, tradeType: dto.TakeProfit2.TradeType, ticker: dto.TakeProfit2.Ticker, quantity: dto.TakeProfit2.Quantity, price: dto.TakeProfit2.Price, leverage: dto.TakeProfit2.Leverage, exchangeOrderId: dto.TakeProfit2.ExchangeOrderId, message: dto.TakeProfit2.Message); + position.TakeProfit2 = new Trade(date: dto.TakeProfit2.Date, direction: dto.TakeProfit2.Direction, + status: dto.TakeProfit2.Status, tradeType: dto.TakeProfit2.TradeType, ticker: dto.TakeProfit2.Ticker, + quantity: dto.TakeProfit2.Quantity, price: dto.TakeProfit2.Price, leverage: dto.TakeProfit2.Leverage, + exchangeOrderId: dto.TakeProfit2.ExchangeOrderId, message: dto.TakeProfit2.Message); } return position; @@ -302,6 +321,7 @@ public static class MongoMappers #endregion #region Signals + public static SignalDto Map(Signal signal) { return new SignalDto @@ -330,7 +350,8 @@ public static class MongoMappers #endregion - #region Scenarios + #region Scenarios + public static ScenarioDto Map(Scenario scenario) { return new ScenarioDto() @@ -373,6 +394,7 @@ public static class MongoMappers CyclePeriods = strategyDto.CyclePeriods }; } + internal static StrategyDto Map(Strategy strategy) { var dto = new StrategyDto @@ -430,6 +452,7 @@ public static class MongoMappers #endregion #region Money Management + public static MoneyManagementDto Map(MoneyManagement request) { if (request == null) return null; @@ -497,16 +520,25 @@ public static class MongoMappers Scenario = new ScenarioDto { Name = spotlight.Scenario.Name, - Strategies = spotlight.Scenario.Strategies.ConvertAll(spotlightScenarioStrategy => Map(spotlightScenarioStrategy)) + Strategies = + spotlight.Scenario.Strategies.ConvertAll( + spotlightScenarioStrategy => Map(spotlightScenarioStrategy)) }, TickerSignals = spotlight.TickerSignals.ConvertAll(spotlightTickerSignal => new TickerSignalDto { Ticker = spotlightTickerSignal.Ticker, - FiveMinutes = spotlightTickerSignal.FiveMinutes?.ConvertAll(spotlightTickerSignalFiveMinute => Map(spotlightTickerSignalFiveMinute)) ?? new List(), - FifteenMinutes = spotlightTickerSignal.FifteenMinutes?.ConvertAll(spotlightTickerSignalFifteenMinute => Map(spotlightTickerSignalFifteenMinute)) ?? new List(), - OneHour = spotlightTickerSignal.OneHour?.ConvertAll(spotlightTickerSignalOneHour => Map(spotlightTickerSignalOneHour)) ?? new List(), - FourHour = spotlightTickerSignal.FourHour?.ConvertAll(spotlightTickerSignalFourHour => Map(spotlightTickerSignalFourHour)) ?? new List(), - OneDay = spotlightTickerSignal.OneDay?.ConvertAll(spotlightTickerSignalOneDay => Map(spotlightTickerSignalOneDay)) ?? new List() + FiveMinutes = + spotlightTickerSignal.FiveMinutes?.ConvertAll(spotlightTickerSignalFiveMinute => + Map(spotlightTickerSignalFiveMinute)) ?? new List(), + FifteenMinutes = + spotlightTickerSignal.FifteenMinutes?.ConvertAll(spotlightTickerSignalFifteenMinute => + Map(spotlightTickerSignalFifteenMinute)) ?? new List(), + OneHour = spotlightTickerSignal.OneHour?.ConvertAll(spotlightTickerSignalOneHour => + Map(spotlightTickerSignalOneHour)) ?? new List(), + FourHour = spotlightTickerSignal.FourHour?.ConvertAll(spotlightTickerSignalFourHour => + Map(spotlightTickerSignalFourHour)) ?? new List(), + OneDay = spotlightTickerSignal.OneDay?.ConvertAll(spotlightTickerSignalOneDay => + Map(spotlightTickerSignalOneDay)) ?? new List() }) }); } @@ -528,16 +560,23 @@ public static class MongoMappers { Scenario = new Scenario(name: spotlight.Scenario.Name) { - Strategies = spotlight.Scenario.Strategies.ConvertAll(spotlightScenarioStrategy => Map(spotlightScenarioStrategy)) + Strategies = + spotlight.Scenario.Strategies.ConvertAll( + spotlightScenarioStrategy => Map(spotlightScenarioStrategy)) }, TickerSignals = spotlight.TickerSignals.ConvertAll(spotlightTickerSignal => new TickerSignal { Ticker = spotlightTickerSignal.Ticker, - FiveMinutes = spotlightTickerSignal.FiveMinutes.ConvertAll(spotlightTickerSignalFiveMinute => Map(spotlightTickerSignalFiveMinute)), - FifteenMinutes = spotlightTickerSignal.FifteenMinutes.ConvertAll(spotlightTickerSignalFifteenMinute => Map(spotlightTickerSignalFifteenMinute)), - OneHour = spotlightTickerSignal.OneHour.ConvertAll(spotlightTickerSignalOneHour => Map(spotlightTickerSignalOneHour)), - FourHour = spotlightTickerSignal.FourHour.ConvertAll(spotlightTickerSignalFourHour => Map(spotlightTickerSignalFourHour)), - OneDay = spotlightTickerSignal.OneDay.ConvertAll(spotlightTickerSignalOneDay => Map(spotlightTickerSignalOneDay)) + FiveMinutes = spotlightTickerSignal.FiveMinutes.ConvertAll(spotlightTickerSignalFiveMinute => + Map(spotlightTickerSignalFiveMinute)), + FifteenMinutes = spotlightTickerSignal.FifteenMinutes.ConvertAll(spotlightTickerSignalFifteenMinute => + Map(spotlightTickerSignalFifteenMinute)), + OneHour = spotlightTickerSignal.OneHour.ConvertAll(spotlightTickerSignalOneHour => + Map(spotlightTickerSignalOneHour)), + FourHour = spotlightTickerSignal.FourHour.ConvertAll(spotlightTickerSignalFourHour => + Map(spotlightTickerSignalFourHour)), + OneDay = spotlightTickerSignal.OneDay.ConvertAll(spotlightTickerSignalOneDay => + Map(spotlightTickerSignalOneDay)) }) }); } @@ -675,4 +714,31 @@ public static class MongoMappers } #endregion -} + + public static FundingRate Map(FundingRateDto fundingRate) + { + if (fundingRate == null) + return null; + + return new FundingRate + { + Exchange = fundingRate.Exchange, + Rate = fundingRate.Rate, + Ticker = fundingRate.Ticker, + Date = fundingRate.Date, + Direction = fundingRate.Direction + }; + } + + public static FundingRateDto Map(FundingRate fundingRate) + { + return new FundingRateDto + { + Exchange = fundingRate.Exchange, + Rate = fundingRate.Rate, + Ticker = fundingRate.Ticker, + Date = fundingRate.Date, + Direction = fundingRate.Direction + }; + } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/StatisticRepository.cs b/src/Managing.Infrastructure.Database/StatisticRepository.cs index 7de54b7..d76f4ea 100644 --- a/src/Managing.Infrastructure.Database/StatisticRepository.cs +++ b/src/Managing.Infrastructure.Database/StatisticRepository.cs @@ -8,21 +8,24 @@ namespace Managing.Infrastructure.Databases; public class StatisticRepository : IStatisticRepository { - private readonly IMongoRepository _bestTrader; + private readonly IMongoRepository _bestTrader; private readonly IMongoRepository _badTrader; private readonly IMongoRepository _topRepository; private readonly IMongoRepository _spotlightRepository; + private readonly IMongoRepository _fundingRateRepository; public StatisticRepository( IMongoRepository topRepository, IMongoRepository spotlightRepository, IMongoRepository bestTrader, - IMongoRepository badTrader) + IMongoRepository badTrader, + IMongoRepository fundingRateRepository) { _topRepository = topRepository; _spotlightRepository = spotlightRepository; _bestTrader = bestTrader; _badTrader = badTrader; + _fundingRateRepository = fundingRateRepository; } public async Task InsertTopVolumeTicker(TopVolumeTicker topVolumeTicker) @@ -62,7 +65,7 @@ public class StatisticRepository : IStatisticRepository public void UpdateBestTrader(Trader trader) { - var t = _bestTrader.FindOne(t => t.Address == trader.Address); + var t = _bestTrader.FindOne(t => t.Address == trader.Address); var dto = MongoMappers.Map(trader); dto.Id = t.Id; _bestTrader.Update(dto); @@ -100,4 +103,28 @@ public class StatisticRepository : IStatisticRepository { await _badTrader.DeleteOneAsync(t => t.Address == trader.Address); } -} + + public List GetFundingRates() + { + return _fundingRateRepository.FindAll().Select(MongoMappers.Map).ToList(); + } + + public async Task RemoveFundingRate(FundingRate oldRate) + { + var rate = _fundingRateRepository.FindOne(r => r.Ticker == oldRate.Ticker && r.Exchange == oldRate.Exchange); + await _fundingRateRepository.DeleteOneAsync(r => r.Id == rate.Id); + } + + public async Task InsertFundingRate(FundingRate newRate) + { + await _fundingRateRepository.InsertOneAsync(MongoMappers.Map(newRate)); + } + + public void UpdateFundingRate(FundingRate oldRate, FundingRate newRate) + { + var f = _fundingRateRepository.FindOne(t => t.Ticker == oldRate.Ticker && t.Exchange == oldRate.Exchange); + var dto = MongoMappers.Map(newRate); + dto.Id = f.Id; + _fundingRateRepository.Update(dto); + } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs b/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs index 1746891..b7193e7 100644 --- a/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs @@ -1,6 +1,7 @@ using Managing.Common; using Managing.Domain.Accounts; using Managing.Domain.Candles; +using Managing.Domain.Statistics; using Managing.Domain.Trades; using static Managing.Common.Enums; @@ -36,4 +37,5 @@ public interface IExchangeProcessor Orderbook GetOrderbook(Account account, Ticker ticker); Task> GetOrders(Account account, Ticker ticker); Task GetTrade(string reference, string orderId, Ticker ticker); + Task> GetFundingRates(); } diff --git a/src/Managing.Infrastructure.Exchanges/ExchangeService.cs b/src/Managing.Infrastructure.Exchanges/ExchangeService.cs index d33e1c6..5e581a7 100644 --- a/src/Managing.Infrastructure.Exchanges/ExchangeService.cs +++ b/src/Managing.Infrastructure.Exchanges/ExchangeService.cs @@ -1,11 +1,12 @@ -using Managing.Domain.Trades; +using Managing.Application.Abstractions.Repositories; +using Managing.Application.Abstractions.Services; +using Managing.Domain.Accounts; using Managing.Domain.Candles; +using Managing.Domain.Statistics; +using Managing.Domain.Trades; +using Managing.Infrastructure.Exchanges.Abstractions; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; -using Managing.Domain.Accounts; -using Managing.Application.Abstractions.Repositories; -using Managing.Application.Abstractions.Services; -using Managing.Infrastructure.Exchanges.Abstractions; namespace Managing.Infrastructure.Exchanges { @@ -145,6 +146,12 @@ namespace Managing.Infrastructure.Exchanges return await processor.GetTrade(reference, orderId, ticker); } + public Task> GetFundingRates() + { + var processor = _exchangeProcessor.First(e => e.Exchange() == Common.Enums.TradingExchanges.Evm); + return processor.GetFundingRates(); + } + public async Task> GetTrades(Account account, Ticker ticker) { var processor = GetProcessor(account); diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs index 6403980..dd91d07 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs @@ -1,6 +1,7 @@ using Managing.Common; using Managing.Domain.Accounts; using Managing.Domain.Candles; +using Managing.Domain.Statistics; using Managing.Domain.Trades; using Managing.Infrastructure.Exchanges.Abstractions; using static Managing.Common.Enums; @@ -26,5 +27,6 @@ namespace Managing.Infrastructure.Exchanges.Exchanges public abstract Task> GetBalances(Account account, bool isForPaperTrading = false); public abstract Task> GetOrders(Account account, Ticker ticker); public abstract Task GetTrade(string reference, string orderId, Ticker ticker); + public abstract Task> GetFundingRates(); } } diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/BinanceProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/BinanceProcessor.cs index 1516b8e..2c2253a 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/BinanceProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/BinanceProcessor.cs @@ -6,6 +6,7 @@ using Managing.Common; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Candles; +using Managing.Domain.Statistics; using Managing.Domain.Trades; using Managing.Infrastructure.Exchanges.Helpers; using Microsoft.Extensions.Logging; @@ -98,6 +99,11 @@ public class BinanceProcessor : BaseProcessor throw new NotImplementedException(); } + public override Task> GetFundingRates() + { + throw new NotImplementedException(); + } + public override async Task> GetTrades(Account account, Ticker ticker) { var binanceOrder = diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs index 8f2a4a5..b5c087e 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs @@ -3,6 +3,7 @@ using Managing.Common; using Managing.Domain.Accounts; using Managing.Domain.Candles; using Managing.Domain.Evm; +using Managing.Domain.Statistics; using Managing.Domain.Trades; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -96,6 +97,11 @@ public class EvmProcessor : BaseProcessor return await _evmManager.GetTrade(reference, Constants.Chains.Arbitrum, ticker); } + public override async Task> GetFundingRates() + { + return await _evmManager.GetFundingRates(); + } + public override decimal GetVolume(Account account, Ticker ticker) { var volume = _evmManager.GetVolume(SubgraphProvider.ChainlinkPrice, ticker); diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/FtxProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/FtxProcessor.cs index 01fcd5c..d088a61 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/FtxProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/FtxProcessor.cs @@ -4,6 +4,7 @@ using FTX.Net.Objects; using Managing.Common; using Managing.Domain.Accounts; using Managing.Domain.Candles; +using Managing.Domain.Statistics; using Managing.Domain.Trades; using Managing.Infrastructure.Exchanges.Helpers; using Microsoft.Extensions.Logging; @@ -194,4 +195,9 @@ public class FtxProcessor : BaseProcessor { throw new NotImplementedException(); } + + public override Task> GetFundingRates() + { + throw new NotImplementedException(); + } } diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/KrakenProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/KrakenProcessor.cs index 4309a44..d36f868 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/KrakenProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/KrakenProcessor.cs @@ -5,6 +5,7 @@ using Kraken.Net.Objects.Options; using Managing.Common; using Managing.Domain.Accounts; using Managing.Domain.Candles; +using Managing.Domain.Statistics; using Managing.Domain.Trades; using Managing.Infrastructure.Exchanges.Helpers; using Microsoft.Extensions.Logging; @@ -92,6 +93,11 @@ public class KrakenProcessor : BaseProcessor throw new NotImplementedException(); } + public override Task> GetFundingRates() + { + throw new NotImplementedException(); + } + public override async Task> GetTrades(Account account, Ticker ticker) { LoadClient(account); diff --git a/src/Managing.Infrastructure.Exchanges/Helpers/BinanceHelpers.cs b/src/Managing.Infrastructure.Exchanges/Helpers/BinanceHelpers.cs index 3f0f32c..965e394 100644 --- a/src/Managing.Infrastructure.Exchanges/Helpers/BinanceHelpers.cs +++ b/src/Managing.Infrastructure.Exchanges/Helpers/BinanceHelpers.cs @@ -19,7 +19,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers return new Trade(data.CreateTime, (data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short, - (TradeStatus)data.Status, (TradeType)data.OriginalType, MiscExtensions.ParseEnum(data.Symbol), data.Quantity, data.AveragePrice, 1, + (TradeStatus)data.Status, (TradeType)data.OriginalType, MiscExtensions.ParseEnum(data.Symbol), + data.Quantity, data.AveragePrice, 1, data.ClientOrderId, ""); } @@ -68,8 +69,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers } return new Trade(DateTime.Now, TradeDirection.None, - TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0, - "", result.Error?.Message); + TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0, + "", result.Error?.Message); } public static Candle Map(IBinanceKline binanceKline, Ticker ticker, Enums.TradingExchanges exchange) @@ -115,16 +116,12 @@ namespace Managing.Infrastructure.Exchanges.Helpers return "ATOMUSDT"; case Ticker.AVAX: return "AVAXUSDT"; - case Ticker.BAT: - return "BATUSDT"; case Ticker.BNB: return "BNBUSDT"; case Ticker.BTC: return "BTCUSDT"; case Ticker.CRV: return "CRVUSDT"; - case Ticker.DEFI: - return "DEFIUSDT"; case Ticker.DOGE: return "DOGEUSDT"; case Ticker.DOT: @@ -143,8 +140,6 @@ namespace Managing.Infrastructure.Exchanges.Helpers return "GRTUSDT"; case Ticker.IMX: return "IMXUSDT"; - case Ticker.KAVA: - return "KAVAUSDT"; case Ticker.KSM: return "KSMUSDT"; case Ticker.LINK: @@ -159,10 +154,6 @@ namespace Managing.Infrastructure.Exchanges.Helpers return "MKRUSDT"; case Ticker.NEAR: return "NEARUSDT"; - case Ticker.NEO: - return "NEOUSDT"; - case Ticker.ONT: - return "ONTUSDT"; case Ticker.SAND: return "SANDUSDT"; case Ticker.SOL: @@ -175,19 +166,16 @@ namespace Managing.Infrastructure.Exchanges.Helpers return "THETAUSDT"; case Ticker.UNI: return "UNIUSDT"; - case Ticker.WAVES: - return "WAVESUSDT"; case Ticker.XMR: return "XMRUSDT"; case Ticker.XRP: return "XRPUSDT"; case Ticker.XTZ: return "XTZUSDT"; - case Ticker.ZEC: - return "ZECUSDT"; default: break; } + throw new NotImplementedException(); } @@ -200,4 +188,4 @@ namespace Managing.Infrastructure.Exchanges.Helpers public class BinanceFuturesPlacedOrder { } -} +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Exchanges/Helpers/FtxHelpers.cs b/src/Managing.Infrastructure.Exchanges/Helpers/FtxHelpers.cs index 3fcfe1c..7f18f97 100644 --- a/src/Managing.Infrastructure.Exchanges/Helpers/FtxHelpers.cs +++ b/src/Managing.Infrastructure.Exchanges/Helpers/FtxHelpers.cs @@ -19,28 +19,18 @@ namespace Managing.Infrastructure.Exchanges.Helpers return "ADA-PERP"; case Ticker.APE: return "APE-PERP"; - case Ticker.ALICE: - return "ALICE-PERP"; case Ticker.ALGO: return "ALGO-PERP"; case Ticker.ATOM: return "ATOM-PERP"; case Ticker.AVAX: return "AVAX-PERP"; - case Ticker.AXS: - return "AXS-PERP"; - case Ticker.BAT: - return "BAT-PERP"; case Ticker.BNB: return "BNB-PERP"; case Ticker.BTC: return "BTC-PERP"; case Ticker.BAL: return "BAL-PERP"; - case Ticker.C98: - return "C98-PERP"; - case Ticker.CHR: - return "CHR-PERP"; case Ticker.CHZ: return "CHZ-PERP"; case Ticker.COMP: @@ -49,10 +39,6 @@ namespace Managing.Infrastructure.Exchanges.Helpers return "CRO-PERP"; case Ticker.CRV: return "CRV-PERP"; - case Ticker.CVC: - return "CVC-PERP"; - case Ticker.DEFI: - return "DEFI-PERP"; case Ticker.DOGE: return "DOGE-PERP"; case Ticker.DOT: @@ -73,26 +59,14 @@ namespace Managing.Infrastructure.Exchanges.Helpers return "FTM-PERP"; case Ticker.GALA: return "GALA-PERP"; - case Ticker.GMT: - return "GMT-PERP"; case Ticker.GRT: return "GRT-PERP"; - case Ticker.HNT: - return "HNT-PERP"; - case Ticker.IMX: - return "IMX-PERP"; - case Ticker.JASMY: - return "JASMY-PERP"; - case Ticker.KAVA: - return "KAVA-PERP"; case Ticker.KSM: return "KSM-PERP"; case Ticker.LDO: return "LDO-PERP"; case Ticker.LINK: return "LINK-PERP"; - case Ticker.LOOKS: - return "LOOKS-PERP"; case Ticker.LRC: return "LRC-PERP"; case Ticker.LTC: @@ -105,18 +79,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers return "MKR-PERP"; case Ticker.NEAR: return "NEAR-PERP"; - case Ticker.NEO: - return "NEO-PERP"; - case Ticker.OMG: - return "OMG-PERP"; - case Ticker.ONE: - return "ONE-PERP"; - case Ticker.ONT: - return "ONT-PERP"; case Ticker.QTUM: return "QTUM-PERP"; - case Ticker.REEF: - return "REEF-PERP"; case Ticker.REN: return "REN-PERP"; case Ticker.ROSE: @@ -131,35 +95,22 @@ namespace Managing.Infrastructure.Exchanges.Helpers return "SOL-PERP"; case Ticker.SRM: return "SRM-PERP"; - case Ticker.STMX: - return "STMX-PERP"; case Ticker.SUSHI: return "SUSHI-PERP"; - case Ticker.SXP: - return "SXP-PERP"; case Ticker.THETA: return "THETA-PERP"; case Ticker.UNI: return "UNI-PERP"; - case Ticker.VET: - return "VET-PERP"; - case Ticker.WAVES: - return "WAVES-PERP"; case Ticker.XMR: return "XMR-PERP"; case Ticker.XRP: return "XRP-PERP"; case Ticker.XTZ: return "XTZ-PERP"; - case Ticker.YFI: - return "YFI-PERP"; - case Ticker.ZEC: - return "ZEC-PERP"; - case Ticker.ZIL: - return "ZIL-PERP"; default: break; } + throw new NotImplementedException(); } @@ -177,7 +128,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers return new Trade(data.CreateTime, (data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short, - (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum(data.Symbol), data.Quantity, data.AverageFillPrice ?? 0, leverage, + (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum(data.Symbol), + data.Quantity, data.AverageFillPrice ?? 0, leverage, data.ClientOrderId, ""); } @@ -194,7 +146,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers return new Trade(data.CreateTime, (data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short, - (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum(data.Symbol), data.Quantity, data.TriggerPrice ?? 0, leverage, + (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum(data.Symbol), + data.Quantity, data.TriggerPrice ?? 0, leverage, Guid.NewGuid().ToString(), ""); } @@ -217,7 +170,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers return null; return new Trade(data.CreateTime, TradeDirection.None, - (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum(data.Symbol), data.Quantity, data.AverageFillPrice ?? 0, 0, + (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum(data.Symbol), + data.Quantity, data.AverageFillPrice ?? 0, 0, data.ClientOrderId, ""); } @@ -280,7 +234,7 @@ namespace Managing.Infrastructure.Exchanges.Helpers private static List Map(IEnumerable entry) { - return entry.Select(ask => new OrderBookEntry() { Price = ask.Price, Quantity = ask.Quantity}).ToList(); + return entry.Select(ask => new OrderBookEntry() { Price = ask.Price, Quantity = ask.Quantity }).ToList(); } } -} +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Messengers/Discord/DiscordHelpers.cs b/src/Managing.Infrastructure.Messengers/Discord/DiscordHelpers.cs index bd2e298..c5d48c2 100644 --- a/src/Managing.Infrastructure.Messengers/Discord/DiscordHelpers.cs +++ b/src/Managing.Infrastructure.Messengers/Discord/DiscordHelpers.cs @@ -16,7 +16,8 @@ public static class DiscordHelpers fields.Add(new EmbedFieldBuilder { Name = $"{GetExplorerUrl(trader.Address)}", - Value = $"Avg Win / Avg Loss / Winrate / ROI \n {trader.AverageWin:#.##}$ / {trader.AverageLoss:#.##}$ / {trader.Winrate}% / {Convert.ToDecimal(trader.Roi) * 100:#.##}%", + Value = + $"Avg Win / Avg Loss / Winrate / ROI \n {trader.AverageWin:#.##}$ / {trader.AverageLoss:#.##}$ / {trader.Winrate}% / {Convert.ToDecimal(trader.Roi) * 100:#.##}%", }); } @@ -31,6 +32,48 @@ public static class DiscordHelpers return embed; } + public static Embed GetFundingRateEmbed(FundingRate fundingRate, string title, FundingRate? oldRate = null) + { + var fields = new List(); + + decimal ratePerYear = fundingRate.Rate; // Rate per year + decimal ratePerDay = ratePerYear / 365; // Rate per day + decimal ratePerMonth = ratePerYear / 12; // Rate per month + decimal ratePerHour = ratePerDay / 24; // Rate per hour + + if (oldRate != null) + { + var oldRatePerYear = oldRate.Rate; // Rate per year + var oldRatePerDay = oldRatePerYear / 365; // Rate per day + var oldRatePerMonth = oldRatePerYear / 12; // Rate per month + var oldRatePerHour = oldRatePerDay / 24; // Rate per hour + + fields.Add(new EmbedFieldBuilder + { + Name = $"{fundingRate.Direction} - Previous Rate", + Value = + $"Hour: {oldRatePerHour:#.##}% / Day: {oldRatePerDay:#.##}% / Month: {oldRatePerMonth:#.##}% / Year: {oldRatePerYear:#.##}%", + }); + } + + fields.Add(new EmbedFieldBuilder + { + Name = $"{fundingRate.Direction} - Current rate", + Value = + $"Hour: {ratePerHour:#.##}% / Day: {ratePerDay:#.##}% / Month: {ratePerMonth:#.##}% / Year: {ratePerYear:#.##}%", + }); + + var embed = new EmbedBuilder + { + Author = new EmbedAuthorBuilder() { Name = "GMX" }, + Title = $"{title} {DateTime.UtcNow:d}", + Color = Color.DarkGreen, + Fields = fields, + }.Build(); + + return embed; + } + public static Embed GetEmbed(string address, string title, List fields, Color color) { return new EmbedBuilder @@ -57,14 +100,16 @@ public static class DiscordHelpers fields.Add(new EmbedFieldBuilder { Name = $"{GetExplorerUrl(trade.ExchangeOrderId)}", - Value = $"Side / Ticker / Open / Qty / Leverage / LiqPrice \n {trade.Direction} / {trade.Ticker} / {trade.Price:#.##}$ / {trade.Quantity:#.##}$ / x{trade.Leverage:#.##} / {Convert.ToDecimal(trade.Message):#.##}$", + Value = + $"Side / Ticker / Open / Qty / Leverage / LiqPrice \n {trade.Direction} / {trade.Ticker} / {trade.Price:#.##}$ / {trade.Quantity:#.##}$ / x{trade.Leverage:#.##} / {Convert.ToDecimal(trade.Message):#.##}$", }); } fields.Add(new EmbedFieldBuilder { Name = "Summary", - Value = $"Long / Short / \n {trades.Count(t => t.Direction == Common.Enums.TradeDirection.Long)} / {trades.Count(t => t.Direction == Common.Enums.TradeDirection.Short)}" + Value = + $"Long / Short / \n {trades.Count(t => t.Direction == Common.Enums.TradeDirection.Long)} / {trades.Count(t => t.Direction == Common.Enums.TradeDirection.Short)}" }); var embed = new EmbedBuilder @@ -77,4 +122,34 @@ public static class DiscordHelpers return embed; } -} + + public static Embed GetFundingRatesEmbed(List fundingRates, string leaderboardOpenPosition) + { + var fields = new List(); + + foreach (var fundingRate in fundingRates) + { + decimal ratePerYear = fundingRate.Rate; // Rate per year + decimal ratePerDay = ratePerYear / 365; // Rate per day + decimal ratePerMonth = ratePerYear / 12; // Rate per month + decimal ratePerHour = ratePerDay / 24; // Rate per hour + + fields.Add(new EmbedFieldBuilder + { + Name = $"{fundingRate.Ticker}", + Value = + $"Hour: {ratePerHour:#.##}% / Day: {ratePerDay:#.##}% / Month: {ratePerMonth:#.##}% / Year: {ratePerYear:#.##}%", + }); + } + + var embed = new EmbedBuilder + { + Author = new EmbedAuthorBuilder() { Name = "GMX" }, + Title = $"Best Funding Rate {DateTime.UtcNow:d}", + Color = Color.DarkGreen, + Fields = fields, + }.Build(); + + return embed; + } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Messengers/Discord/DiscordService.cs b/src/Managing.Infrastructure.Messengers/Discord/DiscordService.cs index 2545dab..ec6029b 100644 --- a/src/Managing.Infrastructure.Messengers/Discord/DiscordService.cs +++ b/src/Managing.Infrastructure.Messengers/Discord/DiscordService.cs @@ -48,6 +48,7 @@ namespace Managing.Infrastructure.Messengers.Discord } #region Setup + // The hosted service has started public async Task StartAsync(CancellationToken cancellationToken) { @@ -85,7 +86,9 @@ namespace Managing.Infrastructure.Messengers.Discord case Constants.DiscordSlashCommand.LeaderboardPosition: await SlashCommands.HandleLeadboardPositionCommand(_services, command); break; - + case Constants.DiscordSlashCommand.FundingRates: + await SlashCommands.HandleFundingRateCommand(_services, command); + break; } } @@ -117,6 +120,10 @@ namespace Managing.Infrastructure.Messengers.Discord noobiesboardCommand.WithDescription("Shows the last Noobies board"); applicationCommandProperties.Add(noobiesboardCommand.Build()); + var fundingRatesCommand = new SlashCommandBuilder(); + fundingRatesCommand.WithName(Constants.DiscordSlashCommand.FundingRates); + fundingRatesCommand.WithDescription("Shows the last funding rates"); + applicationCommandProperties.Add(fundingRatesCommand.Build()); await _client.BulkOverwriteGlobalApplicationCommandsAsync(applicationCommandProperties.ToArray()); } @@ -131,7 +138,6 @@ namespace Managing.Infrastructure.Messengers.Discord { List commands = new(); - return commands; } @@ -139,10 +145,7 @@ namespace Managing.Infrastructure.Messengers.Discord // logging private async Task Log(LogMessage arg) { - await Task.Run(() => - { - _logger.LogInformation(arg.ToString()); - }); + await Task.Run(() => { _logger.LogInformation(arg.ToString()); }); } private async Task CommandExecuted(Optional command, ICommandContext context, IResult result) @@ -160,6 +163,7 @@ namespace Managing.Infrastructure.Messengers.Discord await Log(new LogMessage(LogSeverity.Error, nameof(CommandExecuted), $"Error: {result.ErrorReason}")); return; } + // react to message await context.Message.AddReactionAsync(new Emoji("🤖")); // robot emoji } @@ -177,13 +181,16 @@ namespace Managing.Infrastructure.Messengers.Discord _client.ButtonExecuted -= ButtonHandler; _client.SlashCommandExecuted -= SlashCommandHandler; } + public void Dispose() { _client?.Dispose(); } + #endregion #region In + public async Task ButtonHandler(SocketMessageComponent component) { var parameters = component.Data.CustomId.Split(new[] { _separator }, StringSplitOptions.None); @@ -191,7 +198,8 @@ namespace Managing.Infrastructure.Messengers.Discord if (component.User.GlobalName != "crypto_saitama") { - await component.Channel.SendMessageAsync("Sorry bro, this feature is not accessible for you.. Do not hesitate to send me approx. 456 121 $ and i give you full access"); + await component.Channel.SendMessageAsync( + "Sorry bro, this feature is not accessible for you.. Do not hesitate to send me approx. 456 121 $ and i give you full access"); } else { @@ -219,7 +227,10 @@ namespace Managing.Infrastructure.Messengers.Discord var json = MiscExtensions.Base64Decode(parameters[1]); var trade = JsonConvert.DeserializeObject(json); - await OpenPosition(component, trade.AccountName, trade.MoneyManagementName, PositionInitiator.CopyTrading, trade.Ticker, trade.Direction, Timeframe.FifteenMinutes, DateTime.Now.AddMinutes(trade.ExpirationMinute), true, trade.Leverage); ; + await OpenPosition(component, trade.AccountName, trade.MoneyManagementName, PositionInitiator.CopyTrading, + trade.Ticker, trade.Direction, Timeframe.FifteenMinutes, + DateTime.Now.AddMinutes(trade.ExpirationMinute), true, trade.Leverage); + ; } public async Task OpenPosition(SocketMessageComponent component, string[] parameters) @@ -234,19 +245,24 @@ namespace Managing.Infrastructure.Messengers.Discord var moneyManagementName = parameters[5]; var expiration = DateTime.Parse(parameters[6]); - await OpenPosition(component, accountName, moneyManagementName, PositionInitiator.User, ticker, direction, timeframe, expiration, false); + await OpenPosition(component, accountName, moneyManagementName, PositionInitiator.User, ticker, direction, + timeframe, expiration, false); } - private async Task OpenPosition(SocketMessageComponent component, string accountName, string moneyManagement, PositionInitiator initiator, Ticker ticker, TradeDirection direction, Timeframe timeframe, DateTime expiration, bool ignoreSLTP, decimal? leverage = null) + private async Task OpenPosition(SocketMessageComponent component, string accountName, string moneyManagement, + PositionInitiator initiator, Ticker ticker, TradeDirection direction, Timeframe timeframe, + DateTime expiration, bool ignoreSLTP, decimal? leverage = null) { if (DateTime.Now > expiration) { - await component.Channel.SendMessageAsync("Sorry I can't open position because you tried to click on a expired button."); + await component.Channel.SendMessageAsync( + "Sorry I can't open position because you tried to click on a expired button."); } else { var exchangeService = (IExchangeService)_services.GetService(typeof(IExchangeService)); - var moneyManagementService = (IMoneyManagementService)_services.GetService(typeof(IMoneyManagementService)); + var moneyManagementService = + (IMoneyManagementService)_services.GetService(typeof(IMoneyManagementService)); var accountService = (IAccountService)_services.GetService(typeof(IAccountService)); var tradingService = (ITradingService)_services.GetService(typeof(ITradingService)); @@ -261,7 +277,8 @@ namespace Managing.Infrastructure.Messengers.Discord var position = await new OpenPositionCommandHandler(exchangeService, accountService, tradingService) .Handle(tradeCommand); - var builder = new ComponentBuilder().WithButton("Close Position", $"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Identifier}"); + var builder = new ComponentBuilder().WithButton("Close Position", + $"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Identifier}"); await component.Channel.SendMessageAsync(MessengerHelpers.GetPositionMessage(position), components: builder.Build()); @@ -272,10 +289,10 @@ namespace Managing.Infrastructure.Messengers.Discord private string GetClosingPositionMessage(Position position) { return $"Closing : {position.OriginDirection} {position.Open.Ticker} \n" + - $"Open Price : {position.Open.Price} \n" + - $"Closing Price : {position.Open.Price} \n" + - $"Quantity :{position.Open.Quantity} \n" + - $"PNL : {position.ProfitAndLoss.Net} $"; + $"Open Price : {position.Open.Price} \n" + + $"Closing Price : {position.Open.Price} \n" + + $"Quantity :{position.Open.Quantity} \n" + + $"PNL : {position.ProfitAndLoss.Net} $"; } private async Task ClosePosition(SocketMessageComponent component, string[] parameters) @@ -287,42 +304,45 @@ namespace Managing.Infrastructure.Messengers.Discord await component.RespondAsync("Alright, let met few seconds to close this position"); var position = _tradingService.GetPositionByIdentifier(parameters[1]); var command = new ClosePositionCommand(position); - var result = await new ClosePositionCommandHandler(exchangeService, accountService, tradingService).Handle(command); + var result = + await new ClosePositionCommandHandler(exchangeService, accountService, tradingService).Handle(command); var fields = new List() { - new EmbedFieldBuilder - { - Name = "Direction", - Value = position.OriginDirection, - IsInline = true - }, - new EmbedFieldBuilder - { - Name = "Open Price", - Value = $"{position.Open.Price:#.##}", - IsInline = true - }, - new EmbedFieldBuilder - { - Name = "Quantity", - Value = $"{position.Open.Quantity:#.##}", - IsInline = true - }, - new EmbedFieldBuilder - { - Name = "Pnl", - Value = $"{position.ProfitAndLoss.Net:#.##}", - IsInline = true - }, + new EmbedFieldBuilder + { + Name = "Direction", + Value = position.OriginDirection, + IsInline = true + }, + new EmbedFieldBuilder + { + Name = "Open Price", + Value = $"{position.Open.Price:#.##}", + IsInline = true + }, + new EmbedFieldBuilder + { + Name = "Quantity", + Value = $"{position.Open.Quantity:#.##}", + IsInline = true + }, + new EmbedFieldBuilder + { + Name = "Pnl", + Value = $"{position.ProfitAndLoss.Net:#.##}", + IsInline = true + }, }; - var embed = DiscordHelpers.GetEmbed(position.AccountName, $"Position status is now {result.Status}", fields, position.ProfitAndLoss.Net > 0 ? Color.Green : Color.Red); + var embed = DiscordHelpers.GetEmbed(position.AccountName, $"Position status is now {result.Status}", fields, + position.ProfitAndLoss.Net > 0 ? Color.Green : Color.Red); await component.Channel.SendMessageAsync("", embed: embed); } - + #endregion #region Out + public async Task SendSignal(string message) { var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel; @@ -330,18 +350,21 @@ namespace Managing.Infrastructure.Messengers.Discord await channel.SendMessageAsync(message, components: builder.Build()); } - public async Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, Timeframe timeframe) + public async Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, + Timeframe timeframe) { var expirationDate = DateTime.Now.AddMinutes(_settings.ButtonExpirationMinutes).ToString("G"); var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel; - var builder = new ComponentBuilder().WithButton("Open Position", $"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}"); + var builder = new ComponentBuilder().WithButton("Open Position", + $"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}"); await channel.SendMessageAsync(message, components: builder.Build()); } - public async Task SendIncreasePosition(string address, Trade trade, string copyAccountName, Trade? oldTrade = null) + public async Task SendIncreasePosition(string address, Trade trade, string copyAccountName, + Trade? oldTrade = null) { var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel; - + var fields = new List() { @@ -373,7 +396,10 @@ namespace Managing.Infrastructure.Messengers.Discord if (oldTrade != null) { - fields.Add(new EmbedFieldBuilder { Name = "Increasy by", Value = $"{(trade.Quantity - oldTrade.Quantity) / trade.Leverage:#.##} $" }); + fields.Add(new EmbedFieldBuilder + { + Name = "Increasy by", Value = $"{(trade.Quantity - oldTrade.Quantity) / trade.Leverage:#.##} $" + }); } var titlePrefix = oldTrade != null ? "Increase " : ""; @@ -398,24 +424,29 @@ namespace Managing.Infrastructure.Messengers.Discord if (oldTrade == null) { - builder.WithButton($"Copy with {mm.Name}", $"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}"); + builder.WithButton($"Copy with {mm.Name}", + $"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}"); } else { - builder.WithButton($"Increase with {mm.Name}", $"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}"); + builder.WithButton($"Increase with {mm.Name}", + $"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}"); } } - - var embed = DiscordHelpers.GetEmbed(address, $"{titlePrefix}{trade.Direction} {trade.Ticker}", fields, trade.Direction == TradeDirection.Long ? Color.Green : Color.Red); + + var embed = DiscordHelpers.GetEmbed(address, $"{titlePrefix}{trade.Direction} {trade.Ticker}", fields, + trade.Direction == TradeDirection.Long ? Color.Green : Color.Red); await channel.SendMessageAsync("", components: builder.Build(), embed: embed); } - public async Task SendPosition(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, Timeframe timeframe) + public async Task SendPosition(string message, TradingExchanges exchange, Ticker ticker, + TradeDirection direction, Timeframe timeframe) { var expirationDate = DateTime.Now.AddMinutes(_settings.ButtonExpirationMinutes).ToString("G"); var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel; - var builder = new ComponentBuilder().WithButton("Open Position", $"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}"); + var builder = new ComponentBuilder().WithButton("Open Position", + $"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}"); await channel.SendMessageAsync(message, components: builder.Build()); } @@ -433,7 +464,9 @@ namespace Managing.Infrastructure.Messengers.Discord public async Task SendTradeMessage(string message, bool isBadBehavior = false) { - var channel = _client.GetChannel(isBadBehavior ? _settings.TroublesChannelId : _settings.TradesChannelId) as IMessageChannel; + var channel = + _client.GetChannel(isBadBehavior ? _settings.TroublesChannelId : _settings.TradesChannelId) as + IMessageChannel; await channel.SendMessageAsync(message); } @@ -467,7 +500,8 @@ namespace Managing.Infrastructure.Messengers.Discord } }; - var embed = DiscordHelpers.GetEmbed(address, $"Closed {oldTrade.Direction} {oldTrade.Ticker}", fields, oldTrade.Direction == TradeDirection.Long ? Color.DarkGreen : Color.DarkRed); + var embed = DiscordHelpers.GetEmbed(address, $"Closed {oldTrade.Direction} {oldTrade.Ticker}", fields, + oldTrade.Direction == TradeDirection.Long ? Color.DarkGreen : Color.DarkRed); var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel; await channel.SendMessageAsync("", embed: embed); } @@ -508,7 +542,8 @@ namespace Managing.Infrastructure.Messengers.Discord } }; - var embed = DiscordHelpers.GetEmbed(address, $"Decrease {trade.Direction} {trade.Ticker}", fields, Color.Blue); + var embed = DiscordHelpers.GetEmbed(address, $"Decrease {trade.Direction} {trade.Ticker}", fields, + Color.Blue); var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel; await channel.SendMessageAsync("", embed: embed); } @@ -517,7 +552,8 @@ namespace Managing.Infrastructure.Messengers.Discord public async Task SendPosition(Position position) { var channel = _client.GetChannel(_settings.TradesChannelId) as IMessageChannel; - var builder = new ComponentBuilder().WithButton("Close Position", $"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Open.ExchangeOrderId}"); + var builder = new ComponentBuilder().WithButton("Close Position", + $"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Open.ExchangeOrderId}"); await channel.SendMessageAsync(MessengerHelpers.GetPositionMessage(position), components: builder.Build()); } @@ -525,7 +561,6 @@ namespace Managing.Infrastructure.Messengers.Discord { var channel = _client.GetChannel(_settings.LeaderboardChannelId) as IMessageChannel; await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Leaderboard")); - } public async Task SendBadTraders(List traders) @@ -534,24 +569,37 @@ namespace Managing.Infrastructure.Messengers.Discord await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Noobiesboard")); } + public async Task SendDowngradedFundingRate(FundingRate fundingRate) + { + var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel; + await channel.SendMessageAsync("", + embed: DiscordHelpers.GetFundingRateEmbed(fundingRate, "Funding rate new opportunity")); + } + + public Task SendNewTopFundingRate(FundingRate newRate) + { + var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel; + return channel.SendMessageAsync("", + embed: DiscordHelpers.GetFundingRateEmbed(newRate, "Funding rate new opportunity")); + } + + public Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate) + { + var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel; + return channel.SendMessageAsync("", + embed: DiscordHelpers.GetFundingRateEmbed(newRate, "Funding rate new opportunity", oldRate)); + } + #endregion public class CopyTradeData { - [JsonProperty(PropertyName = "D")] - public TradeDirection Direction { get; set; } - [JsonProperty(PropertyName = "T")] - public Ticker Ticker { get; set; } - [JsonProperty(PropertyName = "A")] - public string AccountName { get; set; } - [JsonProperty(PropertyName = "E")] - public int ExpirationMinute { get; set; } - [JsonProperty(PropertyName = "L")] - public decimal Leverage { get; set; } - [JsonProperty(PropertyName = "M")] - public string MoneyManagementName { get; internal set; } + [JsonProperty(PropertyName = "D")] public TradeDirection Direction { get; set; } + [JsonProperty(PropertyName = "T")] public Ticker Ticker { get; set; } + [JsonProperty(PropertyName = "A")] public string AccountName { get; set; } + [JsonProperty(PropertyName = "E")] public int ExpirationMinute { get; set; } + [JsonProperty(PropertyName = "L")] public decimal Leverage { get; set; } + [JsonProperty(PropertyName = "M")] public string MoneyManagementName { get; internal set; } } - - } -} +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Messengers/Discord/DiscordSettings.cs b/src/Managing.Infrastructure.Messengers/Discord/DiscordSettings.cs index 3315998..7708f64 100644 --- a/src/Managing.Infrastructure.Messengers/Discord/DiscordSettings.cs +++ b/src/Managing.Infrastructure.Messengers/Discord/DiscordSettings.cs @@ -14,6 +14,7 @@ namespace Managing.Infrastructure.Messengers.Discord RequestsChannelId = config.GetValue("Discord:RequestsChannelId"); LeaderboardChannelId = config.GetValue("Discord:LeaderboardChannelId"); NoobiesboardChannelId = config.GetValue("Discord:NoobiesboardChannelId"); + FundingRateChannelId = config.GetValue("Discord:FundingRateChannelId"); ButtonExpirationMinutes = config.GetValue("Discord:ButtonExpirationMinutes"); HandleUserAction = config.GetValue("Discord:HandleUserAction"); BotActivity = config.GetValue("Discord:BotActivity"); @@ -32,6 +33,6 @@ namespace Managing.Infrastructure.Messengers.Discord public bool BotEnabled { get; set; } public ulong LeaderboardChannelId { get; set; } public ulong NoobiesboardChannelId { get; set; } - + public ulong FundingRateChannelId { get; set; } } -} +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Messengers/Discord/SlashCommands.cs b/src/Managing.Infrastructure.Messengers/Discord/SlashCommands.cs index aab398e..4744552 100644 --- a/src/Managing.Infrastructure.Messengers/Discord/SlashCommands.cs +++ b/src/Managing.Infrastructure.Messengers/Discord/SlashCommands.cs @@ -1,5 +1,6 @@ using Discord.WebSocket; using Managing.Application.Workers.Abstractions; +using Managing.Domain.Statistics; namespace Managing.Infrastructure.Messengers.Discord; @@ -23,6 +24,15 @@ public static class SlashCommands { var statisticService = (IStatisticService)service.GetService(typeof(IStatisticService)); var trades = await statisticService.GetLeadboardPositons(); - await command.FollowupAsync(embed: DiscordHelpers.GetTradesEmbed(trades, "Leaderboard Open position"), ephemeral: true); + await command.FollowupAsync(embed: DiscordHelpers.GetTradesEmbed(trades, "Leaderboard Open position"), + ephemeral: true); } -} + + public static async Task HandleFundingRateCommand(IServiceProvider service, SocketSlashCommand command) + { + var statisticService = (IStatisticService)service.GetService(typeof(IStatisticService)); + List fundingRates = await statisticService.GetFundingRates(); + await command.FollowupAsync( + embed: DiscordHelpers.GetFundingRatesEmbed(fundingRates, "Leaderboard Open position"), ephemeral: true); + } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Tests/TradaoTests.cs b/src/Managing.Infrastructure.Tests/TradaoTests.cs index b81ff8f..9f30075 100644 --- a/src/Managing.Infrastructure.Tests/TradaoTests.cs +++ b/src/Managing.Infrastructure.Tests/TradaoTests.cs @@ -6,8 +6,15 @@ namespace Managing.Infrastructure.Tests; public class TradaoTests { + private readonly TradaoService _tradaoService; + + public TradaoTests() + { + _tradaoService = new TradaoService(); + } + [Fact] - public async void Should_return_best_trader() + public async void Should_return_best_trader() { var service = new TradaoService(); var details = await service.GetBestTrader(); @@ -21,4 +28,39 @@ public class TradaoTests var details = (await service.GetBadTrader()).FindBadTrader(); Assert.NotNull(details); } -} + + [Fact] + public async void Should_return_funding_rates() + { + var service = new TradaoService(); + var details = await service.GetFundingRates(); + Assert.NotNull(details); + } + + [Fact] + public void GetApy_WithPositiveRate_ReturnsExpectedPositiveApy() + { + var result = _tradaoService.GetApy("0.0055"); + Assert.True(result > 48m); + } + + [Fact] + public void GetApy_WithNegativeRate_ReturnsPositiveApy() + { + var result = _tradaoService.GetApy("-0.00834530"); + Assert.True(result < -73m); + } + + [Fact] + public void GetApy_WithZeroRate_ReturnsZeroApy() + { + var result = _tradaoService.GetApy("0"); + Assert.Equal(0m, result); + } + + [Fact] + public void GetApy_WithInvalidFormat_ThrowsFormatException() + { + Assert.Throws(() => _tradaoService.GetApy("invalid")); + } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/EvmManager.cs b/src/Managing.Infrastructure.Web3/EvmManager.cs index 2a7c6bd..4e493c1 100644 --- a/src/Managing.Infrastructure.Web3/EvmManager.cs +++ b/src/Managing.Infrastructure.Web3/EvmManager.cs @@ -1,25 +1,26 @@ -using Managing.Application.Abstractions.Repositories; +using System.Net.Http.Json; +using System.Numerics; +using Managing.Application.Abstractions.Repositories; +using Managing.Common; +using Managing.Core; +using Managing.Domain.Accounts; +using Managing.Domain.Candles; using Managing.Domain.Evm; +using Managing.Domain.Statistics; +using Managing.Domain.Trades; +using Managing.Infrastructure.Evm.Abstractions; +using Managing.Infrastructure.Evm.Models.Gmx; +using Managing.Infrastructure.Evm.Referentials; +using Managing.Infrastructure.Evm.Services; +using Managing.Infrastructure.Evm.Services.Gmx; using NBitcoin; using Nethereum.Contracts; +using Nethereum.Contracts.Standards.ERC20.ContractDefinition; +using Nethereum.HdWallet; using Nethereum.Hex.HexTypes; using Nethereum.Signer; using Nethereum.Web3; -using Nethereum.HdWallet; -using System.Numerics; -using System.Net.Http.Json; -using Managing.Infrastructure.Evm.Services; -using Managing.Domain.Candles; -using Managing.Infrastructure.Evm.Abstractions; -using Managing.Core; using static Managing.Common.Enums; -using Managing.Infrastructure.Evm.Services.Gmx; -using Nethereum.Contracts.Standards.ERC20.ContractDefinition; -using Managing.Common; -using Managing.Domain.Trades; -using Managing.Domain.Accounts; -using Managing.Infrastructure.Evm.Models.Gmx; -using Managing.Infrastructure.Evm.Referentials; namespace Managing.Infrastructure.Evm; @@ -590,6 +591,15 @@ public class EvmManager : IEvmManager return await GmxService.GetTrade(web3, reference, ticker); } + public Task> GetFundingRates() + { + // Call GMX v2 + // Call hyperliquid + + // Map the results + return Task.FromResult(new List()); + } + public async Task QuantityInPosition(string chainName, string publicAddress, Ticker ticker) { var chain = ChainService.GetChain(chainName); diff --git a/src/Managing.Infrastructure.Web3/Services/TradaoService.cs b/src/Managing.Infrastructure.Web3/Services/TradaoService.cs index 6d8e0a6..7812000 100644 --- a/src/Managing.Infrastructure.Web3/Services/TradaoService.cs +++ b/src/Managing.Infrastructure.Web3/Services/TradaoService.cs @@ -1,8 +1,10 @@ -using Managing.Application.Abstractions.Services; +using System.Net.Http.Json; +using Managing.Application.Abstractions.Services; +using Managing.Common; +using Managing.Core; using Managing.Domain.Statistics; using Managing.Domain.Trades; using Managing.Infrastructure.Evm.Models; -using System.Net.Http.Json; namespace Managing.Infrastructure.Evm.Services; @@ -10,14 +12,32 @@ public class TradaoService : ITradaoService { private readonly HttpClient _httpClient; + private readonly HashSet _deltaNeutralTickers = new() + { + Enums.Ticker.BTC, + Enums.Ticker.ARB, + Enums.Ticker.ETH, + Enums.Ticker.AVAX, + Enums.Ticker.BNB, + Enums.Ticker.SOL, + Enums.Ticker.LINK, + Enums.Ticker.OP, + Enums.Ticker.UNI, + Enums.Ticker.AAVE, + Enums.Ticker.PEPE, + Enums.Ticker.WIF, + }; + public TradaoService() { - _httpClient = new HttpClient(); ; + _httpClient = new HttpClient(); + ; } public async Task> GetBadTrader() { - var bestTraders = await _httpClient.GetFromJsonAsync($"https://api.tradao.xyz/v1/td/dashboard/42161/gmx/pnlTop/500/asc/2592000/0/?current=1&limit=500&order=asc&window=2592000&chain=42161&exchange=gmx&openPosition=0"); + var bestTraders = await _httpClient.GetFromJsonAsync( + $"https://api.tradao.xyz/v1/td/dashboard/42161/gmx/pnlTop/500/asc/2592000/0/?current=1&limit=500&order=asc&window=2592000&chain=42161&exchange=gmx&openPosition=0"); if (bestTraders == null || bestTraders.row.Count == 0) { @@ -30,7 +50,8 @@ public class TradaoService : ITradaoService public async Task> GetBestTrader() { - var bestTraders = await _httpClient.GetFromJsonAsync($"https://api.tradao.xyz/v1/td/dashboard/42161/gmx/pnlTop/500/desc/2592000/0/?current=1&limit=500&order=desc&window=2592000&chain=42161&exchange=gmx&openPosition=0"); + var bestTraders = await _httpClient.GetFromJsonAsync( + $"https://api.tradao.xyz/v1/td/dashboard/42161/gmx/pnlTop/500/desc/2592000/0/?current=1&limit=500&order=desc&window=2592000&chain=42161&exchange=gmx&openPosition=0"); if (bestTraders == null || bestTraders.row.Count == 0) { @@ -42,7 +63,9 @@ public class TradaoService : ITradaoService public async Task> GetTrades(string address) { - var response = await _httpClient.GetFromJsonAsync($"https://api.tradao.xyz/v1/td/trader/42161/gmx/insights/{address}"); + var response = + await _httpClient.GetFromJsonAsync( + $"https://api.tradao.xyz/v1/td/trader/42161/gmx/insights/{address}"); var trades = new List(); @@ -60,7 +83,7 @@ public class TradaoService : ITradaoService Convert.ToDecimal(position.averagePrice), Convert.ToDecimal(position.position) / Convert.ToDecimal(position.collateral), address, position.liqPrice - ); + ); trades.Add(trade); } @@ -68,12 +91,15 @@ public class TradaoService : ITradaoService return trades; } + private async Task> GetTraderDetails(TradaoList traders) { var result = new List(); foreach (var trader in traders.row) { - var response = await _httpClient.GetFromJsonAsync($"https://api.tradao.xyz/v1/td/trader/42161/gmx/insights/{trader.user}"); + var response = + await _httpClient.GetFromJsonAsync( + $"https://api.tradao.xyz/v1/td/trader/42161/gmx/insights/{trader.user}"); if (response != null) result.Add(Map(response, trader.user)); @@ -95,4 +121,94 @@ public class TradaoService : ITradaoService Roi = Convert.ToDecimal(response.summary.roi) }; } -} + + public async Task> GetFundingRates() + { + _httpClient.DefaultRequestHeaders.Accept.Clear(); + _httpClient.DefaultRequestHeaders.Accept.Add( + new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); + + var response = + await _httpClient.GetFromJsonAsync>("https://api.tradao.xyz/v1/td/fr/all"); + + return Map(response); + } + + private List Map(List? response) + { + if (response == null) + { + return new List(); + } + + var result = new List(); + + foreach (var fundingRate in response) + { + var ticker = Map(fundingRate.Symbol); + + if (ticker == null || fundingRate.ExchangeName != "gmxv2") + continue; + + if (!_deltaNeutralTickers.Contains(ticker.Value)) + continue; + + var longRate = GetApy(fundingRate.LongFundingRate); + var shortRate = GetApy(fundingRate.ShortFundingRate); + result.Add(new FundingRate + { + Ticker = ticker.Value, + Rate = longRate, + Direction = Enums.TradeDirection.Long, + Date = DateTimeOffset.FromUnixTimeSeconds(fundingRate.Timestamp).DateTime + }); + + result.Add(new FundingRate + { + Ticker = ticker.Value, + Rate = shortRate, + Direction = Enums.TradeDirection.Short, + Date = DateTimeOffset.FromUnixTimeSeconds(fundingRate.Timestamp).DateTime + }); + } + + return result; + } + + private Enums.Ticker? Map(string fundingRateSymbol) + { + try + { + return MiscExtensions.ParseEnum(fundingRateSymbol); + } + catch (Exception e) + { + return null; + } + } + + public decimal GetApy(string fundingRate) + { + try + { + decimal rate = Convert.ToDecimal(fundingRate, System.Globalization.CultureInfo.InvariantCulture); + return rate * 100 * 24 * 365; + } + catch (FormatException) + { + // Handle the case where conversion fails due to an incorrect format + // Log the error, return 0, or handle as appropriate for your application + return 0; + } + } + + public class TradaoFundingRate + { + public string Symbol { get; set; } + public string ChainId { get; set; } + public string ExchangeName { get; set; } + public string LongFundingRate { get; set; } + public string ShortFundingRate { get; set; } + public long Timestamp { get; set; } + } +} \ No newline at end of file