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
This commit is contained in:
Oda
2024-07-19 08:31:09 +07:00
committed by GitHub
parent 545c9d8e4a
commit 029ba5f40e
41 changed files with 914 additions and 304 deletions

View File

@@ -17,19 +17,19 @@ jobs:
# npm install # npm install
# npm run build # npm run build
# npm run test # npm run test
- name: Create deploy.tar # - name: Create deploy.tar
uses: a7ul/tar-action@v1.1.0 # uses: a7ul/tar-action@v1.1.0
with: # with:
command: c # command: c
cwd: "./" # cwd: "./"
files: | # files: |
scripts/build_and_run.sh # scripts/build_and_run.sh
captain-definition # captain-definition
outPath: deploy.tar # outPath: deploy.tar
- name: Deploy App to CapRover # - name: Deploy App to CapRover
uses: caprover/deploy-from-github@v1.0.1 # uses: caprover/deploy-from-github@v1.0.1
with: # with:
server: '${{ secrets.CAPROVER_SERVER }}' # server: '${{ secrets.CAPROVER_SERVER }}'
app: '${{ secrets.APP_NAME }}' # app: '${{ secrets.APP_NAME }}'
token: '${{ secrets.MANAGING_APPS }}' # token: '${{ secrets.MANAGING_APPS }}'
#

30
.github/workflows/dotnet.yml vendored Normal file
View File

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

View File

@@ -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
```

View File

@@ -16,7 +16,7 @@ using Serilog.Sinks.Elasticsearch;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.SetBasePath(System.AppContext.BaseDirectory); builder.Configuration.SetBasePath(System.AppContext.BaseDirectory);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 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) => builder.Host.UseSerilog((hostBuilder, loggerConfiguration) =>
{ {
@@ -49,12 +49,12 @@ builder.Services.AddControllers().AddJsonOptions(options =>
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder => builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
{ {
builder builder
.SetIsOriginAllowed((host) => true) .SetIsOriginAllowed((host) => true)
.AllowAnyOrigin() .AllowAnyOrigin()
.WithOrigins("http://localhost:3000/") .WithOrigins("http://localhost:3000/")
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader() .AllowAnyHeader()
.AllowCredentials(); .AllowCredentials();
})); }));
builder.Services.AddSignalR().AddJsonProtocol(); builder.Services.AddSignalR().AddJsonProtocol();
@@ -85,15 +85,18 @@ builder.Services.AddSwaggerGen(options =>
Scheme = "Bearer", Scheme = "Bearer",
BearerFormat = "JWT" BearerFormat = "JWT"
}); });
options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement{ options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
{
{ {
new Microsoft.OpenApi.Models.OpenApiSecurityScheme{ new Microsoft.OpenApi.Models.OpenApiSecurityScheme
Reference = new Microsoft.OpenApi.Models.OpenApiReference{ {
Reference = new Microsoft.OpenApi.Models.OpenApiReference
{
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
Id = "Bearer" Id = "Bearer"
} }
}, },
new string[]{} new string[] { }
} }
}); });
}); });
@@ -112,6 +115,7 @@ builder.Services.AddHostedService<SpotlightWorker>();
builder.Services.AddHostedService<TraderWatcher>(); builder.Services.AddHostedService<TraderWatcher>();
builder.Services.AddHostedService<LeaderboardWorker>(); builder.Services.AddHostedService<LeaderboardWorker>();
builder.Services.AddHostedService<NoobiesboardWorker>(); builder.Services.AddHostedService<NoobiesboardWorker>();
builder.Services.AddHostedService<FundingRatesWatcher>();
// App // App
var app = builder.Build(); var app = builder.Build();

View File

@@ -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<FundingRatesWatcher>
{
private readonly IStatisticService _statisticService;
public FundingRatesWatcher(
ILogger<FundingRatesWatcher> 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();
}
}

View File

@@ -30,6 +30,7 @@
"TradesChannelId": 998374177763491851, "TradesChannelId": 998374177763491851,
"TroublesChannelId": 1015761955321040917, "TroublesChannelId": 1015761955321040917,
"CopyTradingChannelId": 1132022887012909126, "CopyTradingChannelId": 1132022887012909126,
"FundingRateChannelId": 1263566138709774336,
"RequestsChannelId": 1018589494968078356, "RequestsChannelId": 1018589494968078356,
"ButtonExpirationMinutes": 10 "ButtonExpirationMinutes": 10
}, },

View File

@@ -2,7 +2,9 @@
"Authentication": { "Authentication": {
"Schemes": { "Schemes": {
"Bearer": { "Bearer": {
"ValidAudiences": [ "http://localhost:3000/" ], "ValidAudiences": [
"http://localhost:3000/"
],
"ValidIssuer": "Managing" "ValidIssuer": "Managing"
} }
} }
@@ -42,6 +44,7 @@
"TroublesChannelId": 1015761955321040917, "TroublesChannelId": 1015761955321040917,
"CopyTradingChannelId": 1132022887012909126, "CopyTradingChannelId": 1132022887012909126,
"RequestsChannelId": 1018589494968078356, "RequestsChannelId": 1018589494968078356,
"FundingRateChannelId": 1263566138709774336,
"ButtonExpirationMinutes": 10 "ButtonExpirationMinutes": 10
}, },
"AllowedHosts": "*" "AllowedHosts": "*"

View File

@@ -1,6 +1,7 @@
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Evm; using Managing.Domain.Evm;
using Managing.Domain.Statistics;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -33,4 +34,5 @@ public interface IEvmManager
Task<decimal> GetFee(string chainName); Task<decimal> GetFee(string chainName);
Task<List<Trade>> GetOrders(Account account, Ticker ticker); Task<List<Trade>> GetOrders(Account account, Ticker ticker);
Task<Trade> GetTrade(string reference, string arbitrum, Ticker ticker); Task<Trade> GetTrade(string reference, string arbitrum, Ticker ticker);
Task<List<FundingRate>> GetFundingRates();
} }

View File

@@ -17,4 +17,8 @@ public interface IStatisticRepository
void UpdateBadTrader(Trader trader); void UpdateBadTrader(Trader trader);
Task InsertBadTrader(Trader trader); Task InsertBadTrader(Trader trader);
Task RemoveBadTrader(Trader trader); Task RemoveBadTrader(Trader trader);
List<FundingRate> GetFundingRates();
Task RemoveFundingRate(FundingRate oldRate);
Task InsertFundingRate(FundingRate newRate);
void UpdateFundingRate(FundingRate oldRate, FundingRate newRate);
} }

View File

@@ -6,7 +6,9 @@ namespace Managing.Application.Abstractions.Services;
public interface IDiscordService 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 SendPosition(Position position);
Task SendClosingPosition(Position position); Task SendClosingPosition(Position position);
Task SendMessage(string v); Task SendMessage(string v);
@@ -16,4 +18,7 @@ public interface IDiscordService
Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount); Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount);
Task SendBestTraders(List<Trader> traders); Task SendBestTraders(List<Trader> traders);
Task SendBadTraders(List<Trader> traders); Task SendBadTraders(List<Trader> traders);
Task SendDowngradedFundingRate(FundingRate oldRate);
Task SendNewTopFundingRate(FundingRate newRate);
Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate);
} }

View File

@@ -1,7 +1,8 @@
using Managing.Domain.Trades; using Managing.Domain.Accounts;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using static Managing.Common.Enums; using static Managing.Common.Enums;
using Managing.Domain.Accounts;
namespace Managing.Application.Abstractions.Services; namespace Managing.Application.Abstractions.Services;
@@ -19,16 +20,22 @@ public interface IExchangeService
bool isForPaperTrading = false, bool isForPaperTrading = false,
DateTime? currentDate = null, DateTime? currentDate = null,
bool ioc = true); bool ioc = true);
Task<decimal> GetBalance(Account account, bool isForPaperTrading = false); Task<decimal> GetBalance(Account account, bool isForPaperTrading = false);
Task<List<Balance>> GetBalances(Account account, bool isForPaperTrading = false); Task<List<Balance>> GetBalances(Account account, bool isForPaperTrading = false);
decimal GetPrice(Account account, Ticker ticker, DateTime date); decimal GetPrice(Account account, Ticker ticker, DateTime date);
Task<Trade> GetTrade(Account account, string order, Ticker ticker); Task<Trade> GetTrade(Account account, string order, Ticker ticker);
Task<List<Candle>> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe interval); Task<List<Candle>> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe interval);
Task<Trade> OpenStopLoss(Account account, Ticker ticker, TradeDirection originalDirection, decimal stopLossPrice, Task<Trade> OpenStopLoss(Account account, Ticker ticker, TradeDirection originalDirection, decimal stopLossPrice,
decimal quantity, bool isForPaperTrading = false, DateTime? currentDate = null); decimal quantity, bool isForPaperTrading = false, DateTime? currentDate = null);
Task<List<Ticker>> GetTickers(Account account, Timeframe timeframe); Task<List<Ticker>> GetTickers(Account account, Timeframe timeframe);
Task<Trade> OpenTakeProfit(Account account, Ticker ticker, TradeDirection originalDirection, decimal takeProfitPrice,
Task<Trade> OpenTakeProfit(Account account, Ticker ticker, TradeDirection originalDirection,
decimal takeProfitPrice,
decimal quantity, bool isForPaperTrading = false, DateTime? currentDate = null); decimal quantity, bool isForPaperTrading = false, DateTime? currentDate = null);
Task<Trade> ClosePosition(Account account, Position position, decimal lastPrice, bool isForPaperTrading = false); Task<Trade> ClosePosition(Account account, Position position, decimal lastPrice, bool isForPaperTrading = false);
decimal GetVolume(Account account, Ticker ticker); decimal GetVolume(Account account, Ticker ticker);
Task<List<Trade>> GetTrades(Account account, Ticker ticker); Task<List<Trade>> GetTrades(Account account, Ticker ticker);
@@ -36,10 +43,17 @@ public interface IExchangeService
decimal GetFee(Account account, bool isForPaperTrading = false); decimal GetFee(Account account, bool isForPaperTrading = false);
Candle GetCandle(Account account, Ticker ticker, DateTime date); Candle GetCandle(Account account, Ticker ticker, DateTime date);
Task<decimal> GetQuantityInPosition(Account account, Ticker ticker); Task<decimal> GetQuantityInPosition(Account account, Ticker ticker);
Task<List<Candle>> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate, Timeframe timeframe);
Task<List<Candle>> GetCandlesInflux(TradingExchanges exchange, Ticker ticker, DateTime startDate,
Timeframe timeframe);
decimal GetBestPrice(Account account, Ticker ticker, decimal lastPrice, decimal quantity, TradeDirection direction); decimal GetBestPrice(Account account, Ticker ticker, decimal lastPrice, decimal quantity, TradeDirection direction);
Orderbook GetOrderbook(Account account, Ticker ticker); 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<List<Trade>> GetOpenOrders(Account account, Ticker ticker); Task<List<Trade>> GetOpenOrders(Account account, Ticker ticker);
Task<Trade> GetTrade(string reference, string orderId, Ticker ticker); Task<Trade> GetTrade(string reference, string orderId, Ticker ticker);
Task<List<FundingRate>> GetFundingRates();
} }

View File

@@ -6,7 +6,9 @@ namespace Managing.Application.Abstractions.Services;
public interface IMessengerService 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 SendPosition(Position position);
Task SendClosingPosition(Position position); Task SendClosingPosition(Position position);
Task SendMessage(string v); Task SendMessage(string v);
@@ -16,4 +18,7 @@ public interface IMessengerService
Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount); Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount);
Task SendBestTraders(List<Trader> traders); Task SendBestTraders(List<Trader> traders);
Task SendBadTraders(List<Trader> filteredTrader); Task SendBadTraders(List<Trader> filteredTrader);
Task SendDowngradedFundingRate(FundingRate oldRate);
Task SendNewTopFundingRate(FundingRate newRate);
Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate);
} }

View File

@@ -8,4 +8,5 @@ public interface ITradaoService
Task<List<Trader>> GetBadTrader(); Task<List<Trader>> GetBadTrader();
Task<List<Trader>> GetBestTrader(); Task<List<Trader>> GetBestTrader();
Task<List<Trade>> GetTrades(string address); Task<List<Trade>> GetTrades(string address);
Task<List<FundingRate>> GetFundingRates();
} }

View File

@@ -32,4 +32,5 @@ public interface ITradingService
decimal GetFee(Account account, bool isForPaperTrading = false); decimal GetFee(Account account, bool isForPaperTrading = false);
Task WatchTrader(); Task WatchTrader();
IEnumerable<Trader> GetTradersWatch(); IEnumerable<Trader> GetTradersWatch();
void UpdateDeltaNeutralOpportunities();
} }

View File

@@ -16,4 +16,6 @@ public interface IStatisticService
Task UpdateNoobiesboard(); Task UpdateNoobiesboard();
Task UpdateSpotlight(); Task UpdateSpotlight();
Task UpdateTopVolumeTicker(Enums.TradingExchanges exchange, int top); Task UpdateTopVolumeTicker(Enums.TradingExchanges exchange, int top);
Task UpdateFundingRates();
Task<List<FundingRate>> GetFundingRates();
} }

View File

@@ -1,5 +1,4 @@
 using Managing.Application.Workers.Abstractions;
using Managing.Application.Workers.Abstractions;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -40,7 +39,8 @@ public abstract class BaseWorker<T> : BackgroundService where T : class
} }
else 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; _executionCount = worker.ExecutionCount;
} }
@@ -50,7 +50,6 @@ public abstract class BaseWorker<T> : BackgroundService where T : class
{ {
worker = await _workerService.GetWorker(_workerType); worker = await _workerService.GetWorker(_workerType);
//if (true)
if (worker.IsActive) if (worker.IsActive)
{ {
await Run(cancellationToken); await Run(cancellationToken);
@@ -60,11 +59,13 @@ public abstract class BaseWorker<T> : BackgroundService where T : class
} }
else 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); await Task.Delay(_delay);
} }
_logger.LogInformation($"[{_workerType}] Stopped"); _logger.LogInformation($"[{_workerType}] Stopped");
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -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<List<FundingRate>> GetFundingRates()
{
var previousFundingRate = _statisticRepository.GetFundingRates();
return Task.FromResult(previousFundingRate);
}
public IList<TopVolumeTicker> GetLastTopVolumeTicker() public IList<TopVolumeTicker> GetLastTopVolumeTicker()
{ {
var from = DateTime.UtcNow.AddDays(-1); var from = DateTime.UtcNow.AddDays(-1);
@@ -113,9 +170,10 @@ public class StatisticService : IStatisticService
if (overview != null) 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 else
{ {
@@ -188,14 +246,14 @@ public class StatisticService : IStatisticService
}; };
var backtest = _backtester.RunScalpingBotBacktest( var backtest = _backtester.RunScalpingBotBacktest(
account, account,
moneyManagement, moneyManagement,
ticker, ticker,
scenario, scenario,
timeframe, timeframe,
CandleExtensions.GetMinimalDays(timeframe), CandleExtensions.GetMinimalDays(timeframe),
1000, 1000,
isForWatchingOnly: true); isForWatchingOnly: true);
return backtest.Signals; return backtest.Signals;
} }

View File

@@ -44,7 +44,8 @@ public class MessengerService : IMessengerService
await _discordService.SendPosition(position); 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); await _discordService.SendSignal(message, exchange, ticker, direction, timeframe);
} }
@@ -63,4 +64,19 @@ public class MessengerService : IMessengerService
{ {
await _discordService.SendBadTraders(traders); 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);
}
} }

View File

@@ -3,10 +3,10 @@ using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Statistics; using Managing.Domain.Statistics;
using Managing.Domain.Strategies; using Managing.Domain.Strategies;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Domain.Shared.Helpers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -260,6 +260,11 @@ public class TradingService : ITradingService
return watchAccount; return watchAccount;
} }
public void UpdateDeltaNeutralOpportunities()
{
var fundingRates = _exchangeService.GetFundingRates();
}
private async Task ManageTrader(TraderFollowup a, List<Ticker> tickers) private async Task ManageTrader(TraderFollowup a, List<Ticker> tickers)
{ {
var shortAddress = a.Account.Address.Substring(0, 6); var shortAddress = a.Account.Address.Substring(0, 6);

View File

@@ -7,6 +7,7 @@
public const string Leaderboard = "leaderboard"; public const string Leaderboard = "leaderboard";
public const string Noobiesboard = "noobiesboard"; public const string Noobiesboard = "noobiesboard";
public const string LeaderboardPosition = "leaderboardposition"; public const string LeaderboardPosition = "leaderboardposition";
public const string FundingRates = "fundingrates";
} }
public class DiscordButtonAction public class DiscordButtonAction

View File

@@ -113,54 +113,67 @@ public static class Enums
// Summary: // Summary:
// Limit order // Limit order
Limit = 0, Limit = 0,
// //
// Summary: // Summary:
// Symbol order // Symbol order
Market = 1, Market = 1,
// //
// Summary: // Summary:
// Stop market order // Stop market order
StopMarket = 2, StopMarket = 2,
// //
// Summary: // Summary:
// Stop limit order // Stop limit order
StopLimit = 3, StopLimit = 3,
// //
// Summary: // Summary:
// Stop loss order // Stop loss order
StopLoss = 4, StopLoss = 4,
// //
// Summary: // Summary:
// Take profit order // Take profit order
TakeProfit = 5, TakeProfit = 5,
// //
// Summary: // Summary:
// Stop loss profit order // Stop loss profit order
StopLossProfit = 6, StopLossProfit = 6,
// //
// Summary: // Summary:
// Stop loss profit limit order // Stop loss profit limit order
StopLossProfitLimit = 7, StopLossProfitLimit = 7,
// //
// Summary: // Summary:
// Stop loss limit order // Stop loss limit order
StopLossLimit = 8, StopLossLimit = 8,
// //
// Summary: // Summary:
// Take profit limit order // Take profit limit order
TakeProfitLimit = 9, TakeProfitLimit = 9,
// //
// Summary: // Summary:
// Trailing stop order // Trailing stop order
TrailingStop = 10, TrailingStop = 10,
// //
// Summary: // Summary:
// Trailing stop limit order // Trailing stop limit order
TrailingStopLimit = 11, TrailingStopLimit = 11,
// //
// Summary: // Summary:
// Stop loss and limit order // Stop loss and limit order
StopLossAndLimit = 12, StopLossAndLimit = 12,
// //
// Summary: // Summary:
// Settle position // Settle position
@@ -201,22 +214,27 @@ public static class Enums
/// 5m /// 5m
/// </summary> /// </summary>
FiveMinutes, FiveMinutes,
/// <summary> /// <summary>
/// 15m /// 15m
/// </summary> /// </summary>
FifteenMinutes, FifteenMinutes,
/// <summary> /// <summary>
/// 30m /// 30m
/// </summary> /// </summary>
ThirtyMinutes, ThirtyMinutes,
/// <summary> /// <summary>
/// 1h /// 1h
/// </summary> /// </summary>
OneHour, OneHour,
/// <summary> /// <summary>
/// 4h /// 4h
/// </summary> /// </summary>
FourHour, FourHour,
/// <summary> /// <summary>
/// 1d /// 1d
/// </summary> /// </summary>
@@ -225,25 +243,20 @@ public static class Enums
public enum Ticker public enum Ticker
{ {
AAVE,
ADA, ADA,
APE, APE,
ALICE,
ALGO, ALGO,
ARB,
ATOM, ATOM,
AVAX, AVAX,
AXS,
BAT,
BNB, BNB,
BTC, BTC,
BAL, BAL,
C98,
CHR,
CHZ, CHZ,
COMP, COMP,
CRO, CRO,
CRV, CRV,
CVC,
DEFI,
DOGE, DOGE,
DOT, DOT,
DYDX, DYDX,
@@ -254,29 +267,22 @@ public static class Enums
FLM, FLM,
FTM, FTM,
GALA, GALA,
GMT,
GMX, GMX,
GRT, GRT,
HNT,
IMX, IMX,
JASMY, JASMY,
KAVA,
KSM, KSM,
LDO, LDO,
LINK, LINK,
LOOKS,
LRC, LRC,
LTC, LTC,
MANA, MANA,
MATIC, MATIC,
MKR, MKR,
NEAR, NEAR,
NEO, OP,
OMG, PEPE,
ONE,
ONT,
QTUM, QTUM,
REEF,
REN, REN,
ROSE, ROSE,
RSR, RSR,
@@ -284,21 +290,15 @@ public static class Enums
SAND, SAND,
SOL, SOL,
SRM, SRM,
STMX,
SUSHI, SUSHI,
SXP,
THETA, THETA,
UNI, UNI,
USDC, USDC,
USDT, USDT,
VET, WIF,
WAVES,
XMR, XMR,
XRP, XRP,
XTZ, XTZ,
YFI,
ZEC,
ZIL
} }
public enum WorkerType public enum WorkerType
@@ -318,7 +318,8 @@ public static class Enums
TraderWatcher, TraderWatcher,
LeaderboardWorker, LeaderboardWorker,
Noobiesboard, Noobiesboard,
BotManager BotManager,
FundingRatesWatcher
} }
public enum WorkflowUsage public enum WorkflowUsage
@@ -341,5 +342,4 @@ public static class Enums
Position, Position,
MoneyManagement MoneyManagement
} }
} }

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ namespace Managing.Infrastructure.Databases.MongoDb;
public static class MongoMappers public static class MongoMappers
{ {
#region Statistics #region Statistics
internal static TopVolumeTickerDto Map(TopVolumeTicker topVolumeTicker) internal static TopVolumeTickerDto Map(TopVolumeTicker topVolumeTicker)
{ {
return new TopVolumeTickerDto return new TopVolumeTickerDto
@@ -44,6 +45,7 @@ public static class MongoMappers
#endregion #endregion
#region Accounts #region Accounts
internal static AccountDto Map(Account request) internal static AccountDto Map(Account request)
{ {
return new AccountDto return new AccountDto
@@ -84,9 +86,11 @@ public static class MongoMappers
return a; return a;
} }
#endregion #endregion
#region Workers #region Workers
internal static WorkerDto Map(Worker worker) internal static WorkerDto Map(Worker worker)
{ {
return new WorkerDto return new WorkerDto
@@ -118,6 +122,7 @@ public static class MongoMappers
#endregion #endregion
#region Backtests #region Backtests
internal static Backtest Map(BacktestDto b) internal static Backtest Map(BacktestDto b)
{ {
return new Backtest( return new Backtest(
@@ -166,6 +171,7 @@ public static class MongoMappers
#endregion #endregion
#region Candles #region Candles
public static Candle Map(CandleDto candle) public static Candle Map(CandleDto candle)
{ {
if (candle == null) if (candle == null)
@@ -213,10 +219,10 @@ public static class MongoMappers
return candles.ConvertAll(candle => Map(candle)); return candles.ConvertAll(candle => Map(candle));
} }
#endregion #endregion
#region Positions #region Positions
public static PositionDto Map(Position position) public static PositionDto Map(Position position)
{ {
var p = new PositionDto var p = new PositionDto
@@ -267,9 +273,13 @@ public static class MongoMappers
public static Position Map(PositionDto dto) 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 }, ProfitAndLoss = new ProfitAndLoss { Realized = dto.ProfitAndLoss },
Status = dto.Status, Status = dto.Status,
SignalIdentifier = dto.SignalIdentifier, SignalIdentifier = dto.SignalIdentifier,
@@ -278,17 +288,26 @@ public static class MongoMappers
if (dto.StopLoss != null) 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) 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) 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; return position;
@@ -302,6 +321,7 @@ public static class MongoMappers
#endregion #endregion
#region Signals #region Signals
public static SignalDto Map(Signal signal) public static SignalDto Map(Signal signal)
{ {
return new SignalDto return new SignalDto
@@ -331,6 +351,7 @@ public static class MongoMappers
#endregion #endregion
#region Scenarios #region Scenarios
public static ScenarioDto Map(Scenario scenario) public static ScenarioDto Map(Scenario scenario)
{ {
return new ScenarioDto() return new ScenarioDto()
@@ -373,6 +394,7 @@ public static class MongoMappers
CyclePeriods = strategyDto.CyclePeriods CyclePeriods = strategyDto.CyclePeriods
}; };
} }
internal static StrategyDto Map(Strategy strategy) internal static StrategyDto Map(Strategy strategy)
{ {
var dto = new StrategyDto var dto = new StrategyDto
@@ -430,6 +452,7 @@ public static class MongoMappers
#endregion #endregion
#region Money Management #region Money Management
public static MoneyManagementDto Map(MoneyManagement request) public static MoneyManagementDto Map(MoneyManagement request)
{ {
if (request == null) return null; if (request == null) return null;
@@ -497,16 +520,25 @@ public static class MongoMappers
Scenario = new ScenarioDto Scenario = new ScenarioDto
{ {
Name = spotlight.Scenario.Name, 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 TickerSignals = spotlight.TickerSignals.ConvertAll(spotlightTickerSignal => new TickerSignalDto
{ {
Ticker = spotlightTickerSignal.Ticker, Ticker = spotlightTickerSignal.Ticker,
FiveMinutes = spotlightTickerSignal.FiveMinutes?.ConvertAll(spotlightTickerSignalFiveMinute => Map(spotlightTickerSignalFiveMinute)) ?? new List<SignalDto>(), FiveMinutes =
FifteenMinutes = spotlightTickerSignal.FifteenMinutes?.ConvertAll(spotlightTickerSignalFifteenMinute => Map(spotlightTickerSignalFifteenMinute)) ?? new List<SignalDto>(), spotlightTickerSignal.FiveMinutes?.ConvertAll(spotlightTickerSignalFiveMinute =>
OneHour = spotlightTickerSignal.OneHour?.ConvertAll(spotlightTickerSignalOneHour => Map(spotlightTickerSignalOneHour)) ?? new List<SignalDto>(), Map(spotlightTickerSignalFiveMinute)) ?? new List<SignalDto>(),
FourHour = spotlightTickerSignal.FourHour?.ConvertAll(spotlightTickerSignalFourHour => Map(spotlightTickerSignalFourHour)) ?? new List<SignalDto>(), FifteenMinutes =
OneDay = spotlightTickerSignal.OneDay?.ConvertAll(spotlightTickerSignalOneDay => Map(spotlightTickerSignalOneDay)) ?? new List<SignalDto>() spotlightTickerSignal.FifteenMinutes?.ConvertAll(spotlightTickerSignalFifteenMinute =>
Map(spotlightTickerSignalFifteenMinute)) ?? new List<SignalDto>(),
OneHour = spotlightTickerSignal.OneHour?.ConvertAll(spotlightTickerSignalOneHour =>
Map(spotlightTickerSignalOneHour)) ?? new List<SignalDto>(),
FourHour = spotlightTickerSignal.FourHour?.ConvertAll(spotlightTickerSignalFourHour =>
Map(spotlightTickerSignalFourHour)) ?? new List<SignalDto>(),
OneDay = spotlightTickerSignal.OneDay?.ConvertAll(spotlightTickerSignalOneDay =>
Map(spotlightTickerSignalOneDay)) ?? new List<SignalDto>()
}) })
}); });
} }
@@ -528,16 +560,23 @@ public static class MongoMappers
{ {
Scenario = new Scenario(name: spotlight.Scenario.Name) 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 TickerSignals = spotlight.TickerSignals.ConvertAll(spotlightTickerSignal => new TickerSignal
{ {
Ticker = spotlightTickerSignal.Ticker, Ticker = spotlightTickerSignal.Ticker,
FiveMinutes = spotlightTickerSignal.FiveMinutes.ConvertAll(spotlightTickerSignalFiveMinute => Map(spotlightTickerSignalFiveMinute)), FiveMinutes = spotlightTickerSignal.FiveMinutes.ConvertAll(spotlightTickerSignalFiveMinute =>
FifteenMinutes = spotlightTickerSignal.FifteenMinutes.ConvertAll(spotlightTickerSignalFifteenMinute => Map(spotlightTickerSignalFifteenMinute)), Map(spotlightTickerSignalFiveMinute)),
OneHour = spotlightTickerSignal.OneHour.ConvertAll(spotlightTickerSignalOneHour => Map(spotlightTickerSignalOneHour)), FifteenMinutes = spotlightTickerSignal.FifteenMinutes.ConvertAll(spotlightTickerSignalFifteenMinute =>
FourHour = spotlightTickerSignal.FourHour.ConvertAll(spotlightTickerSignalFourHour => Map(spotlightTickerSignalFourHour)), Map(spotlightTickerSignalFifteenMinute)),
OneDay = spotlightTickerSignal.OneDay.ConvertAll(spotlightTickerSignalOneDay => Map(spotlightTickerSignalOneDay)) 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 #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
};
}
} }

View File

@@ -12,17 +12,20 @@ public class StatisticRepository : IStatisticRepository
private readonly IMongoRepository<BadTraderDto> _badTrader; private readonly IMongoRepository<BadTraderDto> _badTrader;
private readonly IMongoRepository<TopVolumeTickerDto> _topRepository; private readonly IMongoRepository<TopVolumeTickerDto> _topRepository;
private readonly IMongoRepository<SpotlighOverviewDto> _spotlightRepository; private readonly IMongoRepository<SpotlighOverviewDto> _spotlightRepository;
private readonly IMongoRepository<FundingRateDto> _fundingRateRepository;
public StatisticRepository( public StatisticRepository(
IMongoRepository<TopVolumeTickerDto> topRepository, IMongoRepository<TopVolumeTickerDto> topRepository,
IMongoRepository<SpotlighOverviewDto> spotlightRepository, IMongoRepository<SpotlighOverviewDto> spotlightRepository,
IMongoRepository<BestTraderDto> bestTrader, IMongoRepository<BestTraderDto> bestTrader,
IMongoRepository<BadTraderDto> badTrader) IMongoRepository<BadTraderDto> badTrader,
IMongoRepository<FundingRateDto> fundingRateRepository)
{ {
_topRepository = topRepository; _topRepository = topRepository;
_spotlightRepository = spotlightRepository; _spotlightRepository = spotlightRepository;
_bestTrader = bestTrader; _bestTrader = bestTrader;
_badTrader = badTrader; _badTrader = badTrader;
_fundingRateRepository = fundingRateRepository;
} }
public async Task InsertTopVolumeTicker(TopVolumeTicker topVolumeTicker) public async Task InsertTopVolumeTicker(TopVolumeTicker topVolumeTicker)
@@ -62,7 +65,7 @@ public class StatisticRepository : IStatisticRepository
public void UpdateBestTrader(Trader trader) 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); var dto = MongoMappers.Map(trader);
dto.Id = t.Id; dto.Id = t.Id;
_bestTrader.Update(dto); _bestTrader.Update(dto);
@@ -100,4 +103,28 @@ public class StatisticRepository : IStatisticRepository
{ {
await _badTrader.DeleteOneAsync(t => t.Address == trader.Address); await _badTrader.DeleteOneAsync(t => t.Address == trader.Address);
} }
public List<FundingRate> 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);
}
} }

View File

@@ -1,6 +1,7 @@
using Managing.Common; using Managing.Common;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Statistics;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -36,4 +37,5 @@ public interface IExchangeProcessor
Orderbook GetOrderbook(Account account, Ticker ticker); Orderbook GetOrderbook(Account account, Ticker ticker);
Task<List<Trade>> GetOrders(Account account, Ticker ticker); Task<List<Trade>> GetOrders(Account account, Ticker ticker);
Task<Trade> GetTrade(string reference, string orderId, Ticker ticker); Task<Trade> GetTrade(string reference, string orderId, Ticker ticker);
Task<List<FundingRate>> GetFundingRates();
} }

View File

@@ -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.Candles;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using Managing.Infrastructure.Exchanges.Abstractions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums; 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 namespace Managing.Infrastructure.Exchanges
{ {
@@ -145,6 +146,12 @@ namespace Managing.Infrastructure.Exchanges
return await processor.GetTrade(reference, orderId, ticker); return await processor.GetTrade(reference, orderId, ticker);
} }
public Task<List<FundingRate>> GetFundingRates()
{
var processor = _exchangeProcessor.First(e => e.Exchange() == Common.Enums.TradingExchanges.Evm);
return processor.GetFundingRates();
}
public async Task<List<Trade>> GetTrades(Account account, Ticker ticker) public async Task<List<Trade>> GetTrades(Account account, Ticker ticker)
{ {
var processor = GetProcessor(account); var processor = GetProcessor(account);

View File

@@ -1,6 +1,7 @@
using Managing.Common; using Managing.Common;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Statistics;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Infrastructure.Exchanges.Abstractions; using Managing.Infrastructure.Exchanges.Abstractions;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -26,5 +27,6 @@ namespace Managing.Infrastructure.Exchanges.Exchanges
public abstract Task<List<Balance>> GetBalances(Account account, bool isForPaperTrading = false); public abstract Task<List<Balance>> GetBalances(Account account, bool isForPaperTrading = false);
public abstract Task<List<Trade>> GetOrders(Account account, Ticker ticker); public abstract Task<List<Trade>> GetOrders(Account account, Ticker ticker);
public abstract Task<Trade> GetTrade(string reference, string orderId, Ticker ticker); public abstract Task<Trade> GetTrade(string reference, string orderId, Ticker ticker);
public abstract Task<List<FundingRate>> GetFundingRates();
} }
} }

View File

@@ -6,6 +6,7 @@ using Managing.Common;
using Managing.Core; using Managing.Core;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Statistics;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Infrastructure.Exchanges.Helpers; using Managing.Infrastructure.Exchanges.Helpers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -98,6 +99,11 @@ public class BinanceProcessor : BaseProcessor
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override Task<List<FundingRate>> GetFundingRates()
{
throw new NotImplementedException();
}
public override async Task<List<Trade>> GetTrades(Account account, Ticker ticker) public override async Task<List<Trade>> GetTrades(Account account, Ticker ticker)
{ {
var binanceOrder = var binanceOrder =

View File

@@ -3,6 +3,7 @@ using Managing.Common;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Evm; using Managing.Domain.Evm;
using Managing.Domain.Statistics;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -96,6 +97,11 @@ public class EvmProcessor : BaseProcessor
return await _evmManager.GetTrade(reference, Constants.Chains.Arbitrum, ticker); return await _evmManager.GetTrade(reference, Constants.Chains.Arbitrum, ticker);
} }
public override async Task<List<FundingRate>> GetFundingRates()
{
return await _evmManager.GetFundingRates();
}
public override decimal GetVolume(Account account, Ticker ticker) public override decimal GetVolume(Account account, Ticker ticker)
{ {
var volume = _evmManager.GetVolume(SubgraphProvider.ChainlinkPrice, ticker); var volume = _evmManager.GetVolume(SubgraphProvider.ChainlinkPrice, ticker);

View File

@@ -4,6 +4,7 @@ using FTX.Net.Objects;
using Managing.Common; using Managing.Common;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Statistics;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Infrastructure.Exchanges.Helpers; using Managing.Infrastructure.Exchanges.Helpers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -194,4 +195,9 @@ public class FtxProcessor : BaseProcessor
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override Task<List<FundingRate>> GetFundingRates()
{
throw new NotImplementedException();
}
} }

View File

@@ -5,6 +5,7 @@ using Kraken.Net.Objects.Options;
using Managing.Common; using Managing.Common;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Statistics;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Infrastructure.Exchanges.Helpers; using Managing.Infrastructure.Exchanges.Helpers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -92,6 +93,11 @@ public class KrakenProcessor : BaseProcessor
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override Task<List<FundingRate>> GetFundingRates()
{
throw new NotImplementedException();
}
public override async Task<List<Trade>> GetTrades(Account account, Ticker ticker) public override async Task<List<Trade>> GetTrades(Account account, Ticker ticker)
{ {
LoadClient(account); LoadClient(account);

View File

@@ -19,7 +19,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return new Trade(data.CreateTime, return new Trade(data.CreateTime,
(data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short, (data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short,
(TradeStatus)data.Status, (TradeType)data.OriginalType, MiscExtensions.ParseEnum<Ticker>(data.Symbol), data.Quantity, data.AveragePrice, 1, (TradeStatus)data.Status, (TradeType)data.OriginalType, MiscExtensions.ParseEnum<Ticker>(data.Symbol),
data.Quantity, data.AveragePrice, 1,
data.ClientOrderId, ""); data.ClientOrderId, "");
} }
@@ -68,8 +69,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers
} }
return new Trade(DateTime.Now, TradeDirection.None, return new Trade(DateTime.Now, TradeDirection.None,
TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0, TradeStatus.Cancelled, TradeType.Market, Ticker.BTC, 0, 0, 0,
"", result.Error?.Message); "", result.Error?.Message);
} }
public static Candle Map(IBinanceKline binanceKline, Ticker ticker, Enums.TradingExchanges exchange) public static Candle Map(IBinanceKline binanceKline, Ticker ticker, Enums.TradingExchanges exchange)
@@ -115,16 +116,12 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "ATOMUSDT"; return "ATOMUSDT";
case Ticker.AVAX: case Ticker.AVAX:
return "AVAXUSDT"; return "AVAXUSDT";
case Ticker.BAT:
return "BATUSDT";
case Ticker.BNB: case Ticker.BNB:
return "BNBUSDT"; return "BNBUSDT";
case Ticker.BTC: case Ticker.BTC:
return "BTCUSDT"; return "BTCUSDT";
case Ticker.CRV: case Ticker.CRV:
return "CRVUSDT"; return "CRVUSDT";
case Ticker.DEFI:
return "DEFIUSDT";
case Ticker.DOGE: case Ticker.DOGE:
return "DOGEUSDT"; return "DOGEUSDT";
case Ticker.DOT: case Ticker.DOT:
@@ -143,8 +140,6 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "GRTUSDT"; return "GRTUSDT";
case Ticker.IMX: case Ticker.IMX:
return "IMXUSDT"; return "IMXUSDT";
case Ticker.KAVA:
return "KAVAUSDT";
case Ticker.KSM: case Ticker.KSM:
return "KSMUSDT"; return "KSMUSDT";
case Ticker.LINK: case Ticker.LINK:
@@ -159,10 +154,6 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "MKRUSDT"; return "MKRUSDT";
case Ticker.NEAR: case Ticker.NEAR:
return "NEARUSDT"; return "NEARUSDT";
case Ticker.NEO:
return "NEOUSDT";
case Ticker.ONT:
return "ONTUSDT";
case Ticker.SAND: case Ticker.SAND:
return "SANDUSDT"; return "SANDUSDT";
case Ticker.SOL: case Ticker.SOL:
@@ -175,19 +166,16 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "THETAUSDT"; return "THETAUSDT";
case Ticker.UNI: case Ticker.UNI:
return "UNIUSDT"; return "UNIUSDT";
case Ticker.WAVES:
return "WAVESUSDT";
case Ticker.XMR: case Ticker.XMR:
return "XMRUSDT"; return "XMRUSDT";
case Ticker.XRP: case Ticker.XRP:
return "XRPUSDT"; return "XRPUSDT";
case Ticker.XTZ: case Ticker.XTZ:
return "XTZUSDT"; return "XTZUSDT";
case Ticker.ZEC:
return "ZECUSDT";
default: default:
break; break;
} }
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@@ -19,28 +19,18 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "ADA-PERP"; return "ADA-PERP";
case Ticker.APE: case Ticker.APE:
return "APE-PERP"; return "APE-PERP";
case Ticker.ALICE:
return "ALICE-PERP";
case Ticker.ALGO: case Ticker.ALGO:
return "ALGO-PERP"; return "ALGO-PERP";
case Ticker.ATOM: case Ticker.ATOM:
return "ATOM-PERP"; return "ATOM-PERP";
case Ticker.AVAX: case Ticker.AVAX:
return "AVAX-PERP"; return "AVAX-PERP";
case Ticker.AXS:
return "AXS-PERP";
case Ticker.BAT:
return "BAT-PERP";
case Ticker.BNB: case Ticker.BNB:
return "BNB-PERP"; return "BNB-PERP";
case Ticker.BTC: case Ticker.BTC:
return "BTC-PERP"; return "BTC-PERP";
case Ticker.BAL: case Ticker.BAL:
return "BAL-PERP"; return "BAL-PERP";
case Ticker.C98:
return "C98-PERP";
case Ticker.CHR:
return "CHR-PERP";
case Ticker.CHZ: case Ticker.CHZ:
return "CHZ-PERP"; return "CHZ-PERP";
case Ticker.COMP: case Ticker.COMP:
@@ -49,10 +39,6 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "CRO-PERP"; return "CRO-PERP";
case Ticker.CRV: case Ticker.CRV:
return "CRV-PERP"; return "CRV-PERP";
case Ticker.CVC:
return "CVC-PERP";
case Ticker.DEFI:
return "DEFI-PERP";
case Ticker.DOGE: case Ticker.DOGE:
return "DOGE-PERP"; return "DOGE-PERP";
case Ticker.DOT: case Ticker.DOT:
@@ -73,26 +59,14 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "FTM-PERP"; return "FTM-PERP";
case Ticker.GALA: case Ticker.GALA:
return "GALA-PERP"; return "GALA-PERP";
case Ticker.GMT:
return "GMT-PERP";
case Ticker.GRT: case Ticker.GRT:
return "GRT-PERP"; 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: case Ticker.KSM:
return "KSM-PERP"; return "KSM-PERP";
case Ticker.LDO: case Ticker.LDO:
return "LDO-PERP"; return "LDO-PERP";
case Ticker.LINK: case Ticker.LINK:
return "LINK-PERP"; return "LINK-PERP";
case Ticker.LOOKS:
return "LOOKS-PERP";
case Ticker.LRC: case Ticker.LRC:
return "LRC-PERP"; return "LRC-PERP";
case Ticker.LTC: case Ticker.LTC:
@@ -105,18 +79,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "MKR-PERP"; return "MKR-PERP";
case Ticker.NEAR: case Ticker.NEAR:
return "NEAR-PERP"; 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: case Ticker.QTUM:
return "QTUM-PERP"; return "QTUM-PERP";
case Ticker.REEF:
return "REEF-PERP";
case Ticker.REN: case Ticker.REN:
return "REN-PERP"; return "REN-PERP";
case Ticker.ROSE: case Ticker.ROSE:
@@ -131,35 +95,22 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "SOL-PERP"; return "SOL-PERP";
case Ticker.SRM: case Ticker.SRM:
return "SRM-PERP"; return "SRM-PERP";
case Ticker.STMX:
return "STMX-PERP";
case Ticker.SUSHI: case Ticker.SUSHI:
return "SUSHI-PERP"; return "SUSHI-PERP";
case Ticker.SXP:
return "SXP-PERP";
case Ticker.THETA: case Ticker.THETA:
return "THETA-PERP"; return "THETA-PERP";
case Ticker.UNI: case Ticker.UNI:
return "UNI-PERP"; return "UNI-PERP";
case Ticker.VET:
return "VET-PERP";
case Ticker.WAVES:
return "WAVES-PERP";
case Ticker.XMR: case Ticker.XMR:
return "XMR-PERP"; return "XMR-PERP";
case Ticker.XRP: case Ticker.XRP:
return "XRP-PERP"; return "XRP-PERP";
case Ticker.XTZ: case Ticker.XTZ:
return "XTZ-PERP"; return "XTZ-PERP";
case Ticker.YFI:
return "YFI-PERP";
case Ticker.ZEC:
return "ZEC-PERP";
case Ticker.ZIL:
return "ZIL-PERP";
default: default:
break; break;
} }
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -177,7 +128,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return new Trade(data.CreateTime, return new Trade(data.CreateTime,
(data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short, (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, (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum<Ticker>(data.Symbol),
data.Quantity, data.AverageFillPrice ?? 0, leverage,
data.ClientOrderId, ""); data.ClientOrderId, "");
} }
@@ -194,7 +146,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return new Trade(data.CreateTime, return new Trade(data.CreateTime,
(data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short, (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, (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum<Ticker>(data.Symbol),
data.Quantity, data.TriggerPrice ?? 0, leverage,
Guid.NewGuid().ToString(), ""); Guid.NewGuid().ToString(), "");
} }
@@ -217,7 +170,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return null; return null;
return new Trade(data.CreateTime, TradeDirection.None, return new Trade(data.CreateTime, TradeDirection.None,
(TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum<Ticker>(data.Symbol), data.Quantity, data.AverageFillPrice ?? 0, 0, (TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum<Ticker>(data.Symbol),
data.Quantity, data.AverageFillPrice ?? 0, 0,
data.ClientOrderId, ""); data.ClientOrderId, "");
} }
@@ -280,7 +234,7 @@ namespace Managing.Infrastructure.Exchanges.Helpers
private static List<OrderBookEntry> Map(IEnumerable<FTXOrderBookEntry> entry) private static List<OrderBookEntry> Map(IEnumerable<FTXOrderBookEntry> 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();
} }
} }
} }

View File

@@ -16,7 +16,8 @@ public static class DiscordHelpers
fields.Add(new EmbedFieldBuilder fields.Add(new EmbedFieldBuilder
{ {
Name = $"{GetExplorerUrl(trader.Address)}", 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; return embed;
} }
public static Embed GetFundingRateEmbed(FundingRate fundingRate, string title, FundingRate? oldRate = null)
{
var fields = new List<EmbedFieldBuilder>();
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<EmbedFieldBuilder> fields, Color color) public static Embed GetEmbed(string address, string title, List<EmbedFieldBuilder> fields, Color color)
{ {
return new EmbedBuilder return new EmbedBuilder
@@ -57,14 +100,16 @@ public static class DiscordHelpers
fields.Add(new EmbedFieldBuilder fields.Add(new EmbedFieldBuilder
{ {
Name = $"{GetExplorerUrl(trade.ExchangeOrderId)}", 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 fields.Add(new EmbedFieldBuilder
{ {
Name = "Summary", 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 var embed = new EmbedBuilder
@@ -77,4 +122,34 @@ public static class DiscordHelpers
return embed; return embed;
} }
public static Embed GetFundingRatesEmbed(List<FundingRate> fundingRates, string leaderboardOpenPosition)
{
var fields = new List<EmbedFieldBuilder>();
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;
}
} }

View File

@@ -48,6 +48,7 @@ namespace Managing.Infrastructure.Messengers.Discord
} }
#region Setup #region Setup
// The hosted service has started // The hosted service has started
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
@@ -85,7 +86,9 @@ namespace Managing.Infrastructure.Messengers.Discord
case Constants.DiscordSlashCommand.LeaderboardPosition: case Constants.DiscordSlashCommand.LeaderboardPosition:
await SlashCommands.HandleLeadboardPositionCommand(_services, command); await SlashCommands.HandleLeadboardPositionCommand(_services, command);
break; 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"); noobiesboardCommand.WithDescription("Shows the last Noobies board");
applicationCommandProperties.Add(noobiesboardCommand.Build()); 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()); await _client.BulkOverwriteGlobalApplicationCommandsAsync(applicationCommandProperties.ToArray());
} }
@@ -132,17 +139,13 @@ namespace Managing.Infrastructure.Messengers.Discord
List<ApplicationCommandProperties> commands = new(); List<ApplicationCommandProperties> commands = new();
return commands; return commands;
} }
// logging // logging
private async Task Log(LogMessage arg) private async Task Log(LogMessage arg)
{ {
await Task.Run(() => await Task.Run(() => { _logger.LogInformation(arg.ToString()); });
{
_logger.LogInformation(arg.ToString());
});
} }
private async Task CommandExecuted(Optional<CommandInfo> command, ICommandContext context, IResult result) private async Task CommandExecuted(Optional<CommandInfo> 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}")); await Log(new LogMessage(LogSeverity.Error, nameof(CommandExecuted), $"Error: {result.ErrorReason}"));
return; return;
} }
// react to message // react to message
await context.Message.AddReactionAsync(new Emoji("🤖")); // robot emoji await context.Message.AddReactionAsync(new Emoji("🤖")); // robot emoji
} }
@@ -177,13 +181,16 @@ namespace Managing.Infrastructure.Messengers.Discord
_client.ButtonExecuted -= ButtonHandler; _client.ButtonExecuted -= ButtonHandler;
_client.SlashCommandExecuted -= SlashCommandHandler; _client.SlashCommandExecuted -= SlashCommandHandler;
} }
public void Dispose() public void Dispose()
{ {
_client?.Dispose(); _client?.Dispose();
} }
#endregion #endregion
#region In #region In
public async Task ButtonHandler(SocketMessageComponent component) public async Task ButtonHandler(SocketMessageComponent component)
{ {
var parameters = component.Data.CustomId.Split(new[] { _separator }, StringSplitOptions.None); 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") 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 else
{ {
@@ -219,7 +227,10 @@ namespace Managing.Infrastructure.Messengers.Discord
var json = MiscExtensions.Base64Decode(parameters[1]); var json = MiscExtensions.Base64Decode(parameters[1]);
var trade = JsonConvert.DeserializeObject<CopyTradeData>(json); var trade = JsonConvert.DeserializeObject<CopyTradeData>(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) public async Task OpenPosition(SocketMessageComponent component, string[] parameters)
@@ -234,19 +245,24 @@ namespace Managing.Infrastructure.Messengers.Discord
var moneyManagementName = parameters[5]; var moneyManagementName = parameters[5];
var expiration = DateTime.Parse(parameters[6]); 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) 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 else
{ {
var exchangeService = (IExchangeService)_services.GetService(typeof(IExchangeService)); 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 accountService = (IAccountService)_services.GetService(typeof(IAccountService));
var tradingService = (ITradingService)_services.GetService(typeof(ITradingService)); var tradingService = (ITradingService)_services.GetService(typeof(ITradingService));
@@ -261,7 +277,8 @@ namespace Managing.Infrastructure.Messengers.Discord
var position = await new OpenPositionCommandHandler(exchangeService, accountService, tradingService) var position = await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
.Handle(tradeCommand); .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), await component.Channel.SendMessageAsync(MessengerHelpers.GetPositionMessage(position),
components: builder.Build()); components: builder.Build());
@@ -272,10 +289,10 @@ namespace Managing.Infrastructure.Messengers.Discord
private string GetClosingPositionMessage(Position position) private string GetClosingPositionMessage(Position position)
{ {
return $"Closing : {position.OriginDirection} {position.Open.Ticker} \n" + return $"Closing : {position.OriginDirection} {position.Open.Ticker} \n" +
$"Open Price : {position.Open.Price} \n" + $"Open Price : {position.Open.Price} \n" +
$"Closing Price : {position.Open.Price} \n" + $"Closing Price : {position.Open.Price} \n" +
$"Quantity :{position.Open.Quantity} \n" + $"Quantity :{position.Open.Quantity} \n" +
$"PNL : {position.ProfitAndLoss.Net} $"; $"PNL : {position.ProfitAndLoss.Net} $";
} }
private async Task ClosePosition(SocketMessageComponent component, string[] parameters) private async Task ClosePosition(SocketMessageComponent component, string[] parameters)
@@ -287,35 +304,37 @@ namespace Managing.Infrastructure.Messengers.Discord
await component.RespondAsync("Alright, let met few seconds to close this position"); await component.RespondAsync("Alright, let met few seconds to close this position");
var position = _tradingService.GetPositionByIdentifier(parameters[1]); var position = _tradingService.GetPositionByIdentifier(parameters[1]);
var command = new ClosePositionCommand(position); 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<EmbedFieldBuilder>() var fields = new List<EmbedFieldBuilder>()
{ {
new EmbedFieldBuilder new EmbedFieldBuilder
{ {
Name = "Direction", Name = "Direction",
Value = position.OriginDirection, Value = position.OriginDirection,
IsInline = true IsInline = true
}, },
new EmbedFieldBuilder new EmbedFieldBuilder
{ {
Name = "Open Price", Name = "Open Price",
Value = $"{position.Open.Price:#.##}", Value = $"{position.Open.Price:#.##}",
IsInline = true IsInline = true
}, },
new EmbedFieldBuilder new EmbedFieldBuilder
{ {
Name = "Quantity", Name = "Quantity",
Value = $"{position.Open.Quantity:#.##}", Value = $"{position.Open.Quantity:#.##}",
IsInline = true IsInline = true
}, },
new EmbedFieldBuilder new EmbedFieldBuilder
{ {
Name = "Pnl", Name = "Pnl",
Value = $"{position.ProfitAndLoss.Net:#.##}", Value = $"{position.ProfitAndLoss.Net:#.##}",
IsInline = true 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); await component.Channel.SendMessageAsync("", embed: embed);
} }
@@ -323,6 +342,7 @@ namespace Managing.Infrastructure.Messengers.Discord
#region Out #region Out
public async Task SendSignal(string message) public async Task SendSignal(string message)
{ {
var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel; var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel;
@@ -330,15 +350,18 @@ namespace Managing.Infrastructure.Messengers.Discord
await channel.SendMessageAsync(message, components: builder.Build()); 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 expirationDate = DateTime.Now.AddMinutes(_settings.ButtonExpirationMinutes).ToString("G");
var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel; 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()); 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 channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel;
@@ -373,7 +396,10 @@ namespace Managing.Infrastructure.Messengers.Discord
if (oldTrade != null) 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 " : ""; var titlePrefix = oldTrade != null ? "Increase " : "";
@@ -398,24 +424,29 @@ namespace Managing.Infrastructure.Messengers.Discord
if (oldTrade == null) 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 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); 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 expirationDate = DateTime.Now.AddMinutes(_settings.ButtonExpirationMinutes).ToString("G");
var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel; 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()); 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) 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); 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; var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel;
await channel.SendMessageAsync("", embed: embed); 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; var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel;
await channel.SendMessageAsync("", embed: embed); await channel.SendMessageAsync("", embed: embed);
} }
@@ -517,7 +552,8 @@ namespace Managing.Infrastructure.Messengers.Discord
public async Task SendPosition(Position position) public async Task SendPosition(Position position)
{ {
var channel = _client.GetChannel(_settings.TradesChannelId) as IMessageChannel; 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()); 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; var channel = _client.GetChannel(_settings.LeaderboardChannelId) as IMessageChannel;
await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Leaderboard")); await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Leaderboard"));
} }
public async Task SendBadTraders(List<Trader> traders) public async Task SendBadTraders(List<Trader> traders)
@@ -534,24 +569,37 @@ namespace Managing.Infrastructure.Messengers.Discord
await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Noobiesboard")); 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 #endregion
public class CopyTradeData public class CopyTradeData
{ {
[JsonProperty(PropertyName = "D")] [JsonProperty(PropertyName = "D")] public TradeDirection Direction { get; set; }
public TradeDirection Direction { get; set; } [JsonProperty(PropertyName = "T")] public Ticker Ticker { get; set; }
[JsonProperty(PropertyName = "T")] [JsonProperty(PropertyName = "A")] public string AccountName { get; set; }
public Ticker Ticker { get; set; } [JsonProperty(PropertyName = "E")] public int ExpirationMinute { get; set; }
[JsonProperty(PropertyName = "A")] [JsonProperty(PropertyName = "L")] public decimal Leverage { get; set; }
public string AccountName { get; set; } [JsonProperty(PropertyName = "M")] public string MoneyManagementName { get; internal 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; }
} }
} }
} }

View File

@@ -14,6 +14,7 @@ namespace Managing.Infrastructure.Messengers.Discord
RequestsChannelId = config.GetValue<ulong>("Discord:RequestsChannelId"); RequestsChannelId = config.GetValue<ulong>("Discord:RequestsChannelId");
LeaderboardChannelId = config.GetValue<ulong>("Discord:LeaderboardChannelId"); LeaderboardChannelId = config.GetValue<ulong>("Discord:LeaderboardChannelId");
NoobiesboardChannelId = config.GetValue<ulong>("Discord:NoobiesboardChannelId"); NoobiesboardChannelId = config.GetValue<ulong>("Discord:NoobiesboardChannelId");
FundingRateChannelId = config.GetValue<ulong>("Discord:FundingRateChannelId");
ButtonExpirationMinutes = config.GetValue<int>("Discord:ButtonExpirationMinutes"); ButtonExpirationMinutes = config.GetValue<int>("Discord:ButtonExpirationMinutes");
HandleUserAction = config.GetValue<bool>("Discord:HandleUserAction"); HandleUserAction = config.GetValue<bool>("Discord:HandleUserAction");
BotActivity = config.GetValue<string>("Discord:BotActivity"); BotActivity = config.GetValue<string>("Discord:BotActivity");
@@ -32,6 +33,6 @@ namespace Managing.Infrastructure.Messengers.Discord
public bool BotEnabled { get; set; } public bool BotEnabled { get; set; }
public ulong LeaderboardChannelId { get; set; } public ulong LeaderboardChannelId { get; set; }
public ulong NoobiesboardChannelId { get; set; } public ulong NoobiesboardChannelId { get; set; }
public ulong FundingRateChannelId { get; set; }
} }
} }

View File

@@ -1,5 +1,6 @@
using Discord.WebSocket; using Discord.WebSocket;
using Managing.Application.Workers.Abstractions; using Managing.Application.Workers.Abstractions;
using Managing.Domain.Statistics;
namespace Managing.Infrastructure.Messengers.Discord; namespace Managing.Infrastructure.Messengers.Discord;
@@ -23,6 +24,15 @@ public static class SlashCommands
{ {
var statisticService = (IStatisticService)service.GetService(typeof(IStatisticService)); var statisticService = (IStatisticService)service.GetService(typeof(IStatisticService));
var trades = await statisticService.GetLeadboardPositons(); 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<FundingRate> fundingRates = await statisticService.GetFundingRates();
await command.FollowupAsync(
embed: DiscordHelpers.GetFundingRatesEmbed(fundingRates, "Leaderboard Open position"), ephemeral: true);
} }
} }

View File

@@ -6,6 +6,13 @@ namespace Managing.Infrastructure.Tests;
public class TradaoTests public class TradaoTests
{ {
private readonly TradaoService _tradaoService;
public TradaoTests()
{
_tradaoService = new TradaoService();
}
[Fact] [Fact]
public async void Should_return_best_trader() public async void Should_return_best_trader()
{ {
@@ -21,4 +28,39 @@ public class TradaoTests
var details = (await service.GetBadTrader()).FindBadTrader(); var details = (await service.GetBadTrader()).FindBadTrader();
Assert.NotNull(details); 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<FormatException>(() => _tradaoService.GetApy("invalid"));
}
} }

View File

@@ -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.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 NBitcoin;
using Nethereum.Contracts; using Nethereum.Contracts;
using Nethereum.Contracts.Standards.ERC20.ContractDefinition;
using Nethereum.HdWallet;
using Nethereum.Hex.HexTypes; using Nethereum.Hex.HexTypes;
using Nethereum.Signer; using Nethereum.Signer;
using Nethereum.Web3; 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 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; namespace Managing.Infrastructure.Evm;
@@ -590,6 +591,15 @@ public class EvmManager : IEvmManager
return await GmxService.GetTrade(web3, reference, ticker); return await GmxService.GetTrade(web3, reference, ticker);
} }
public Task<List<FundingRate>> GetFundingRates()
{
// Call GMX v2
// Call hyperliquid
// Map the results
return Task.FromResult(new List<FundingRate>());
}
public async Task<decimal> QuantityInPosition(string chainName, string publicAddress, Ticker ticker) public async Task<decimal> QuantityInPosition(string chainName, string publicAddress, Ticker ticker)
{ {
var chain = ChainService.GetChain(chainName); var chain = ChainService.GetChain(chainName);

View File

@@ -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.Statistics;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Infrastructure.Evm.Models; using Managing.Infrastructure.Evm.Models;
using System.Net.Http.Json;
namespace Managing.Infrastructure.Evm.Services; namespace Managing.Infrastructure.Evm.Services;
@@ -10,14 +12,32 @@ public class TradaoService : ITradaoService
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly HashSet<Enums.Ticker> _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() public TradaoService()
{ {
_httpClient = new HttpClient(); ; _httpClient = new HttpClient();
;
} }
public async Task<List<Trader>> GetBadTrader() public async Task<List<Trader>> GetBadTrader()
{ {
var bestTraders = await _httpClient.GetFromJsonAsync<TradaoList>($"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<TradaoList>(
$"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) if (bestTraders == null || bestTraders.row.Count == 0)
{ {
@@ -30,7 +50,8 @@ public class TradaoService : ITradaoService
public async Task<List<Trader>> GetBestTrader() public async Task<List<Trader>> GetBestTrader()
{ {
var bestTraders = await _httpClient.GetFromJsonAsync<TradaoList>($"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<TradaoList>(
$"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) if (bestTraders == null || bestTraders.row.Count == 0)
{ {
@@ -42,7 +63,9 @@ public class TradaoService : ITradaoService
public async Task<List<Trade>> GetTrades(string address) public async Task<List<Trade>> GetTrades(string address)
{ {
var response = await _httpClient.GetFromJsonAsync<TradaoUserDetails>($"https://api.tradao.xyz/v1/td/trader/42161/gmx/insights/{address}"); var response =
await _httpClient.GetFromJsonAsync<TradaoUserDetails>(
$"https://api.tradao.xyz/v1/td/trader/42161/gmx/insights/{address}");
var trades = new List<Trade>(); var trades = new List<Trade>();
@@ -60,7 +83,7 @@ public class TradaoService : ITradaoService
Convert.ToDecimal(position.averagePrice), Convert.ToDecimal(position.averagePrice),
Convert.ToDecimal(position.position) / Convert.ToDecimal(position.collateral), Convert.ToDecimal(position.position) / Convert.ToDecimal(position.collateral),
address, position.liqPrice address, position.liqPrice
); );
trades.Add(trade); trades.Add(trade);
} }
@@ -68,12 +91,15 @@ public class TradaoService : ITradaoService
return trades; return trades;
} }
private async Task<List<Trader>> GetTraderDetails(TradaoList traders) private async Task<List<Trader>> GetTraderDetails(TradaoList traders)
{ {
var result = new List<Trader>(); var result = new List<Trader>();
foreach (var trader in traders.row) foreach (var trader in traders.row)
{ {
var response = await _httpClient.GetFromJsonAsync<TradaoUserDetails>($"https://api.tradao.xyz/v1/td/trader/42161/gmx/insights/{trader.user}"); var response =
await _httpClient.GetFromJsonAsync<TradaoUserDetails>(
$"https://api.tradao.xyz/v1/td/trader/42161/gmx/insights/{trader.user}");
if (response != null) if (response != null)
result.Add(Map(response, trader.user)); result.Add(Map(response, trader.user));
@@ -95,4 +121,94 @@ public class TradaoService : ITradaoService
Roi = Convert.ToDecimal(response.summary.roi) Roi = Convert.ToDecimal(response.summary.roi)
}; };
} }
public async Task<List<FundingRate>> GetFundingRates()
{
_httpClient.DefaultRequestHeaders.Accept.Clear();
_httpClient.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
var response =
await _httpClient.GetFromJsonAsync<List<TradaoFundingRate>>("https://api.tradao.xyz/v1/td/fr/all");
return Map(response);
}
private List<FundingRate> Map(List<TradaoFundingRate>? response)
{
if (response == null)
{
return new List<FundingRate>();
}
var result = new List<FundingRate>();
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<Enums.Ticker>(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; }
}
} }