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

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

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,
"TroublesChannelId": 1015761955321040917,
"CopyTradingChannelId": 1132022887012909126,
"FundingRateChannelId": 1263566138709774336,
"RequestsChannelId": 1018589494968078356,
"ButtonExpirationMinutes": 10
},

View File

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

View File

@@ -1,6 +1,7 @@
using Managing.Domain.Accounts;
using Managing.Domain.Candles;
using Managing.Domain.Evm;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using static Managing.Common.Enums;
@@ -33,4 +34,5 @@ public interface IEvmManager
Task<decimal> GetFee(string chainName);
Task<List<Trade>> GetOrders(Account account, 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);
Task InsertBadTrader(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
{
Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, Timeframe timeframe);
Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction,
Timeframe timeframe);
Task SendPosition(Position position);
Task SendClosingPosition(Position position);
Task SendMessage(string v);
@@ -16,4 +18,7 @@ public interface IDiscordService
Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount);
Task SendBestTraders(List<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.Statistics;
using Managing.Domain.Trades;
using static Managing.Common.Enums;
using Managing.Domain.Accounts;
namespace Managing.Application.Abstractions.Services;
@@ -19,16 +20,22 @@ public interface IExchangeService
bool isForPaperTrading = false,
DateTime? currentDate = null,
bool ioc = true);
Task<decimal> GetBalance(Account account, bool isForPaperTrading = false);
Task<List<Balance>> GetBalances(Account account, bool isForPaperTrading = false);
decimal GetPrice(Account account, Ticker ticker, DateTime date);
Task<Trade> GetTrade(Account account, string order, Ticker ticker);
Task<List<Candle>> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe interval);
Task<Trade> OpenStopLoss(Account account, Ticker ticker, TradeDirection originalDirection, decimal stopLossPrice,
decimal quantity, bool isForPaperTrading = false, DateTime? currentDate = null);
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);
Task<Trade> ClosePosition(Account account, Position position, decimal lastPrice, bool isForPaperTrading = false);
decimal GetVolume(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);
Candle GetCandle(Account account, Ticker ticker, DateTime date);
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);
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<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
{
Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, Timeframe timeframe);
Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction,
Timeframe timeframe);
Task SendPosition(Position position);
Task SendClosingPosition(Position position);
Task SendMessage(string v);
@@ -16,4 +18,7 @@ public interface IMessengerService
Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount);
Task SendBestTraders(List<Trader> traders);
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>> GetBestTrader();
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);
Task WatchTrader();
IEnumerable<Trader> GetTradersWatch();
void UpdateDeltaNeutralOpportunities();
}

View File

@@ -16,4 +16,6 @@ public interface IStatisticService
Task UpdateNoobiesboard();
Task UpdateSpotlight();
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.Logging;
using static Managing.Common.Enums;
@@ -40,7 +39,8 @@ public abstract class BaseWorker<T> : BackgroundService where T : class
}
else
{
_logger.LogInformation($"[{_workerType}] Last run : {worker.LastRunTime} - Execution Count : {worker.ExecutionCount}");
_logger.LogInformation(
$"[{_workerType}] Last run : {worker.LastRunTime} - Execution Count : {worker.ExecutionCount}");
_executionCount = worker.ExecutionCount;
}
@@ -49,8 +49,7 @@ public abstract class BaseWorker<T> : BackgroundService where T : class
while (!cancellationToken.IsCancellationRequested)
{
worker = await _workerService.GetWorker(_workerType);
//if (true)
if (worker.IsActive)
{
await Run(cancellationToken);
@@ -60,11 +59,13 @@ public abstract class BaseWorker<T> : BackgroundService where T : class
}
else
{
_logger.LogInformation($"[{_workerType}] Worker not active. Next run at : {DateTime.UtcNow.Add(_delay)}");
_logger.LogInformation(
$"[{_workerType}] Worker not active. Next run at : {DateTime.UtcNow.Add(_delay)}");
}
await Task.Delay(_delay);
}
_logger.LogInformation($"[{_workerType}] Stopped");
}
catch (Exception ex)
@@ -74,4 +75,4 @@ public abstract class BaseWorker<T> : BackgroundService where T : class
}
protected abstract Task Run(CancellationToken cancellationToken);
}
}

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()
{
var from = DateTime.UtcNow.AddDays(-1);
@@ -113,16 +170,17 @@ public class StatisticService : IStatisticService
if (overview != null)
{
if(overview.Spotlights.Count < overview.ScenarioCount)
if (overview.Spotlights.Count < overview.ScenarioCount)
{
_logger.LogInformation($"Spotlights not up to date. {overview.Spotlights.Count}/{overview.ScenarioCount}");
_logger.LogInformation(
$"Spotlights not up to date. {overview.Spotlights.Count}/{overview.ScenarioCount}");
}
else
{
_logger.LogInformation("No need to update spotlights");
return;
}
}
}
else
{
overview = new SpotlightOverview
@@ -188,14 +246,14 @@ public class StatisticService : IStatisticService
};
var backtest = _backtester.RunScalpingBotBacktest(
account,
moneyManagement,
ticker,
scenario,
timeframe,
CandleExtensions.GetMinimalDays(timeframe),
1000,
isForWatchingOnly: true);
account,
moneyManagement,
ticker,
scenario,
timeframe,
CandleExtensions.GetMinimalDays(timeframe),
1000,
isForWatchingOnly: true);
return backtest.Signals;
}
@@ -301,4 +359,4 @@ public class StatisticService : IStatisticService
await _messengerService.SendBadTraders(lastBadTrader);
}
}
}

View File

@@ -44,7 +44,8 @@ public class MessengerService : IMessengerService
await _discordService.SendPosition(position);
}
public async Task SendSignal(string message, Enums.TradingExchanges exchange, Enums.Ticker ticker, Enums.TradeDirection direction, Enums.Timeframe timeframe)
public async Task SendSignal(string message, Enums.TradingExchanges exchange, Enums.Ticker ticker,
Enums.TradeDirection direction, Enums.Timeframe timeframe)
{
await _discordService.SendSignal(message, exchange, ticker, direction, timeframe);
}
@@ -63,4 +64,19 @@ public class MessengerService : IMessengerService
{
await _discordService.SendBadTraders(traders);
}
}
public async Task SendDowngradedFundingRate(FundingRate oldRate)
{
await _discordService.SendDowngradedFundingRate(oldRate);
}
public async Task SendNewTopFundingRate(FundingRate newRate)
{
await _discordService.SendNewTopFundingRate(newRate);
}
public async Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate)
{
await _discordService.SendFundingRateUpdate(oldRate, newRate);
}
}

View File

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

View File

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

View File

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

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

@@ -16,7 +16,8 @@ namespace Managing.Infrastructure.Databases.MongoDb;
public static class MongoMappers
{
#region Statistics
#region Statistics
internal static TopVolumeTickerDto Map(TopVolumeTicker topVolumeTicker)
{
return new TopVolumeTickerDto
@@ -44,6 +45,7 @@ public static class MongoMappers
#endregion
#region Accounts
internal static AccountDto Map(Account request)
{
return new AccountDto
@@ -84,9 +86,11 @@ public static class MongoMappers
return a;
}
#endregion
#region Workers
internal static WorkerDto Map(Worker worker)
{
return new WorkerDto
@@ -118,6 +122,7 @@ public static class MongoMappers
#endregion
#region Backtests
internal static Backtest Map(BacktestDto b)
{
return new Backtest(
@@ -165,7 +170,8 @@ public static class MongoMappers
#endregion
#region Candles
#region Candles
public static Candle Map(CandleDto candle)
{
if (candle == null)
@@ -213,10 +219,10 @@ public static class MongoMappers
return candles.ConvertAll(candle => Map(candle));
}
#endregion
#region Positions
public static PositionDto Map(Position position)
{
var p = new PositionDto
@@ -267,9 +273,13 @@ public static class MongoMappers
public static Position Map(PositionDto dto)
{
var position = new Position(dto.AccountName, originDirection: dto.OriginDirection, dto.Ticker, Map(dto.MoneyManagement), dto.Initiator, dto.Date)
var position = new Position(dto.AccountName, originDirection: dto.OriginDirection, dto.Ticker,
Map(dto.MoneyManagement), dto.Initiator, dto.Date)
{
Open = new Trade(date: dto.Open.Date, direction: dto.Open.Direction, status: dto.Open.Status, tradeType: dto.Open.TradeType, ticker: dto.Open.Ticker, quantity: dto.Open.Quantity, price: dto.Open.Price, leverage: dto.Open.Leverage, exchangeOrderId: dto.Open.ExchangeOrderId, message: dto.Open.Message),
Open = new Trade(date: dto.Open.Date, direction: dto.Open.Direction, status: dto.Open.Status,
tradeType: dto.Open.TradeType, ticker: dto.Open.Ticker, quantity: dto.Open.Quantity,
price: dto.Open.Price, leverage: dto.Open.Leverage, exchangeOrderId: dto.Open.ExchangeOrderId,
message: dto.Open.Message),
ProfitAndLoss = new ProfitAndLoss { Realized = dto.ProfitAndLoss },
Status = dto.Status,
SignalIdentifier = dto.SignalIdentifier,
@@ -278,17 +288,26 @@ public static class MongoMappers
if (dto.StopLoss != null)
{
position.StopLoss = new Trade(date: dto.StopLoss.Date, direction: dto.StopLoss.Direction, status: dto.StopLoss.Status, tradeType: dto.StopLoss.TradeType, ticker: dto.StopLoss.Ticker, quantity: dto.StopLoss.Quantity, price: dto.StopLoss.Price, leverage: dto.StopLoss.Leverage, exchangeOrderId: dto.StopLoss.ExchangeOrderId, message: dto.StopLoss.Message);
position.StopLoss = new Trade(date: dto.StopLoss.Date, direction: dto.StopLoss.Direction,
status: dto.StopLoss.Status, tradeType: dto.StopLoss.TradeType, ticker: dto.StopLoss.Ticker,
quantity: dto.StopLoss.Quantity, price: dto.StopLoss.Price, leverage: dto.StopLoss.Leverage,
exchangeOrderId: dto.StopLoss.ExchangeOrderId, message: dto.StopLoss.Message);
}
if (dto.TakeProfit1 != null)
{
position.TakeProfit1 = new Trade(date: dto.TakeProfit1.Date, direction: dto.TakeProfit1.Direction, status: dto.TakeProfit1.Status, tradeType: dto.TakeProfit1.TradeType, ticker: dto.TakeProfit1.Ticker, quantity: dto.TakeProfit1.Quantity, price: dto.TakeProfit1.Price, leverage: dto.TakeProfit1.Leverage, exchangeOrderId: dto.TakeProfit1.ExchangeOrderId, message: dto.TakeProfit1.Message);
position.TakeProfit1 = new Trade(date: dto.TakeProfit1.Date, direction: dto.TakeProfit1.Direction,
status: dto.TakeProfit1.Status, tradeType: dto.TakeProfit1.TradeType, ticker: dto.TakeProfit1.Ticker,
quantity: dto.TakeProfit1.Quantity, price: dto.TakeProfit1.Price, leverage: dto.TakeProfit1.Leverage,
exchangeOrderId: dto.TakeProfit1.ExchangeOrderId, message: dto.TakeProfit1.Message);
}
if (dto.TakeProfit2 != null)
{
position.TakeProfit2 = new Trade(date: dto.TakeProfit2.Date, direction: dto.TakeProfit2.Direction, status: dto.TakeProfit2.Status, tradeType: dto.TakeProfit2.TradeType, ticker: dto.TakeProfit2.Ticker, quantity: dto.TakeProfit2.Quantity, price: dto.TakeProfit2.Price, leverage: dto.TakeProfit2.Leverage, exchangeOrderId: dto.TakeProfit2.ExchangeOrderId, message: dto.TakeProfit2.Message);
position.TakeProfit2 = new Trade(date: dto.TakeProfit2.Date, direction: dto.TakeProfit2.Direction,
status: dto.TakeProfit2.Status, tradeType: dto.TakeProfit2.TradeType, ticker: dto.TakeProfit2.Ticker,
quantity: dto.TakeProfit2.Quantity, price: dto.TakeProfit2.Price, leverage: dto.TakeProfit2.Leverage,
exchangeOrderId: dto.TakeProfit2.ExchangeOrderId, message: dto.TakeProfit2.Message);
}
return position;
@@ -302,6 +321,7 @@ public static class MongoMappers
#endregion
#region Signals
public static SignalDto Map(Signal signal)
{
return new SignalDto
@@ -330,7 +350,8 @@ public static class MongoMappers
#endregion
#region Scenarios
#region Scenarios
public static ScenarioDto Map(Scenario scenario)
{
return new ScenarioDto()
@@ -373,6 +394,7 @@ public static class MongoMappers
CyclePeriods = strategyDto.CyclePeriods
};
}
internal static StrategyDto Map(Strategy strategy)
{
var dto = new StrategyDto
@@ -430,6 +452,7 @@ public static class MongoMappers
#endregion
#region Money Management
public static MoneyManagementDto Map(MoneyManagement request)
{
if (request == null) return null;
@@ -497,16 +520,25 @@ public static class MongoMappers
Scenario = new ScenarioDto
{
Name = spotlight.Scenario.Name,
Strategies = spotlight.Scenario.Strategies.ConvertAll(spotlightScenarioStrategy => Map(spotlightScenarioStrategy))
Strategies =
spotlight.Scenario.Strategies.ConvertAll(
spotlightScenarioStrategy => Map(spotlightScenarioStrategy))
},
TickerSignals = spotlight.TickerSignals.ConvertAll(spotlightTickerSignal => new TickerSignalDto
{
Ticker = spotlightTickerSignal.Ticker,
FiveMinutes = spotlightTickerSignal.FiveMinutes?.ConvertAll(spotlightTickerSignalFiveMinute => Map(spotlightTickerSignalFiveMinute)) ?? new List<SignalDto>(),
FifteenMinutes = 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>()
FiveMinutes =
spotlightTickerSignal.FiveMinutes?.ConvertAll(spotlightTickerSignalFiveMinute =>
Map(spotlightTickerSignalFiveMinute)) ?? new List<SignalDto>(),
FifteenMinutes =
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)
{
Strategies = spotlight.Scenario.Strategies.ConvertAll(spotlightScenarioStrategy => Map(spotlightScenarioStrategy))
Strategies =
spotlight.Scenario.Strategies.ConvertAll(
spotlightScenarioStrategy => Map(spotlightScenarioStrategy))
},
TickerSignals = spotlight.TickerSignals.ConvertAll(spotlightTickerSignal => new TickerSignal
{
Ticker = spotlightTickerSignal.Ticker,
FiveMinutes = spotlightTickerSignal.FiveMinutes.ConvertAll(spotlightTickerSignalFiveMinute => Map(spotlightTickerSignalFiveMinute)),
FifteenMinutes = spotlightTickerSignal.FifteenMinutes.ConvertAll(spotlightTickerSignalFifteenMinute => Map(spotlightTickerSignalFifteenMinute)),
OneHour = spotlightTickerSignal.OneHour.ConvertAll(spotlightTickerSignalOneHour => Map(spotlightTickerSignalOneHour)),
FourHour = spotlightTickerSignal.FourHour.ConvertAll(spotlightTickerSignalFourHour => Map(spotlightTickerSignalFourHour)),
OneDay = spotlightTickerSignal.OneDay.ConvertAll(spotlightTickerSignalOneDay => Map(spotlightTickerSignalOneDay))
FiveMinutes = spotlightTickerSignal.FiveMinutes.ConvertAll(spotlightTickerSignalFiveMinute =>
Map(spotlightTickerSignalFiveMinute)),
FifteenMinutes = spotlightTickerSignal.FifteenMinutes.ConvertAll(spotlightTickerSignalFifteenMinute =>
Map(spotlightTickerSignalFifteenMinute)),
OneHour = spotlightTickerSignal.OneHour.ConvertAll(spotlightTickerSignalOneHour =>
Map(spotlightTickerSignalOneHour)),
FourHour = spotlightTickerSignal.FourHour.ConvertAll(spotlightTickerSignalFourHour =>
Map(spotlightTickerSignalFourHour)),
OneDay = spotlightTickerSignal.OneDay.ConvertAll(spotlightTickerSignalOneDay =>
Map(spotlightTickerSignalOneDay))
})
});
}
@@ -675,4 +714,31 @@ public static class MongoMappers
}
#endregion
}
public static FundingRate Map(FundingRateDto fundingRate)
{
if (fundingRate == null)
return null;
return new FundingRate
{
Exchange = fundingRate.Exchange,
Rate = fundingRate.Rate,
Ticker = fundingRate.Ticker,
Date = fundingRate.Date,
Direction = fundingRate.Direction
};
}
public static FundingRateDto Map(FundingRate fundingRate)
{
return new FundingRateDto
{
Exchange = fundingRate.Exchange,
Rate = fundingRate.Rate,
Ticker = fundingRate.Ticker,
Date = fundingRate.Date,
Direction = fundingRate.Direction
};
}
}

View File

@@ -8,21 +8,24 @@ namespace Managing.Infrastructure.Databases;
public class StatisticRepository : IStatisticRepository
{
private readonly IMongoRepository<BestTraderDto> _bestTrader;
private readonly IMongoRepository<BestTraderDto> _bestTrader;
private readonly IMongoRepository<BadTraderDto> _badTrader;
private readonly IMongoRepository<TopVolumeTickerDto> _topRepository;
private readonly IMongoRepository<SpotlighOverviewDto> _spotlightRepository;
private readonly IMongoRepository<FundingRateDto> _fundingRateRepository;
public StatisticRepository(
IMongoRepository<TopVolumeTickerDto> topRepository,
IMongoRepository<SpotlighOverviewDto> spotlightRepository,
IMongoRepository<BestTraderDto> bestTrader,
IMongoRepository<BadTraderDto> badTrader)
IMongoRepository<BadTraderDto> badTrader,
IMongoRepository<FundingRateDto> fundingRateRepository)
{
_topRepository = topRepository;
_spotlightRepository = spotlightRepository;
_bestTrader = bestTrader;
_badTrader = badTrader;
_fundingRateRepository = fundingRateRepository;
}
public async Task InsertTopVolumeTicker(TopVolumeTicker topVolumeTicker)
@@ -62,7 +65,7 @@ public class StatisticRepository : IStatisticRepository
public void UpdateBestTrader(Trader trader)
{
var t = _bestTrader.FindOne(t => t.Address == trader.Address);
var t = _bestTrader.FindOne(t => t.Address == trader.Address);
var dto = MongoMappers.Map(trader);
dto.Id = t.Id;
_bestTrader.Update(dto);
@@ -100,4 +103,28 @@ public class StatisticRepository : IStatisticRepository
{
await _badTrader.DeleteOneAsync(t => t.Address == trader.Address);
}
}
public List<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.Domain.Accounts;
using Managing.Domain.Candles;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using static Managing.Common.Enums;
@@ -36,4 +37,5 @@ public interface IExchangeProcessor
Orderbook GetOrderbook(Account account, Ticker ticker);
Task<List<Trade>> GetOrders(Account account, 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.Statistics;
using Managing.Domain.Trades;
using Managing.Infrastructure.Exchanges.Abstractions;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
using Managing.Domain.Accounts;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Infrastructure.Exchanges.Abstractions;
namespace Managing.Infrastructure.Exchanges
{
@@ -145,6 +146,12 @@ namespace Managing.Infrastructure.Exchanges
return await processor.GetTrade(reference, orderId, ticker);
}
public Task<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)
{
var processor = GetProcessor(account);

View File

@@ -1,6 +1,7 @@
using Managing.Common;
using Managing.Domain.Accounts;
using Managing.Domain.Candles;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using Managing.Infrastructure.Exchanges.Abstractions;
using static Managing.Common.Enums;
@@ -26,5 +27,6 @@ namespace Managing.Infrastructure.Exchanges.Exchanges
public abstract Task<List<Balance>> GetBalances(Account account, bool isForPaperTrading = false);
public abstract Task<List<Trade>> GetOrders(Account account, 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.Domain.Accounts;
using Managing.Domain.Candles;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using Managing.Infrastructure.Exchanges.Helpers;
using Microsoft.Extensions.Logging;
@@ -98,6 +99,11 @@ public class BinanceProcessor : BaseProcessor
throw new NotImplementedException();
}
public override Task<List<FundingRate>> GetFundingRates()
{
throw new NotImplementedException();
}
public override async Task<List<Trade>> GetTrades(Account account, Ticker ticker)
{
var binanceOrder =

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,28 +19,18 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "ADA-PERP";
case Ticker.APE:
return "APE-PERP";
case Ticker.ALICE:
return "ALICE-PERP";
case Ticker.ALGO:
return "ALGO-PERP";
case Ticker.ATOM:
return "ATOM-PERP";
case Ticker.AVAX:
return "AVAX-PERP";
case Ticker.AXS:
return "AXS-PERP";
case Ticker.BAT:
return "BAT-PERP";
case Ticker.BNB:
return "BNB-PERP";
case Ticker.BTC:
return "BTC-PERP";
case Ticker.BAL:
return "BAL-PERP";
case Ticker.C98:
return "C98-PERP";
case Ticker.CHR:
return "CHR-PERP";
case Ticker.CHZ:
return "CHZ-PERP";
case Ticker.COMP:
@@ -49,10 +39,6 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "CRO-PERP";
case Ticker.CRV:
return "CRV-PERP";
case Ticker.CVC:
return "CVC-PERP";
case Ticker.DEFI:
return "DEFI-PERP";
case Ticker.DOGE:
return "DOGE-PERP";
case Ticker.DOT:
@@ -73,26 +59,14 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "FTM-PERP";
case Ticker.GALA:
return "GALA-PERP";
case Ticker.GMT:
return "GMT-PERP";
case Ticker.GRT:
return "GRT-PERP";
case Ticker.HNT:
return "HNT-PERP";
case Ticker.IMX:
return "IMX-PERP";
case Ticker.JASMY:
return "JASMY-PERP";
case Ticker.KAVA:
return "KAVA-PERP";
case Ticker.KSM:
return "KSM-PERP";
case Ticker.LDO:
return "LDO-PERP";
case Ticker.LINK:
return "LINK-PERP";
case Ticker.LOOKS:
return "LOOKS-PERP";
case Ticker.LRC:
return "LRC-PERP";
case Ticker.LTC:
@@ -105,18 +79,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "MKR-PERP";
case Ticker.NEAR:
return "NEAR-PERP";
case Ticker.NEO:
return "NEO-PERP";
case Ticker.OMG:
return "OMG-PERP";
case Ticker.ONE:
return "ONE-PERP";
case Ticker.ONT:
return "ONT-PERP";
case Ticker.QTUM:
return "QTUM-PERP";
case Ticker.REEF:
return "REEF-PERP";
case Ticker.REN:
return "REN-PERP";
case Ticker.ROSE:
@@ -131,35 +95,22 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return "SOL-PERP";
case Ticker.SRM:
return "SRM-PERP";
case Ticker.STMX:
return "STMX-PERP";
case Ticker.SUSHI:
return "SUSHI-PERP";
case Ticker.SXP:
return "SXP-PERP";
case Ticker.THETA:
return "THETA-PERP";
case Ticker.UNI:
return "UNI-PERP";
case Ticker.VET:
return "VET-PERP";
case Ticker.WAVES:
return "WAVES-PERP";
case Ticker.XMR:
return "XMR-PERP";
case Ticker.XRP:
return "XRP-PERP";
case Ticker.XTZ:
return "XTZ-PERP";
case Ticker.YFI:
return "YFI-PERP";
case Ticker.ZEC:
return "ZEC-PERP";
case Ticker.ZIL:
return "ZIL-PERP";
default:
break;
}
throw new NotImplementedException();
}
@@ -177,7 +128,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return new Trade(data.CreateTime,
(data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short,
(TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum<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, "");
}
@@ -194,7 +146,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return new Trade(data.CreateTime,
(data.Side == OrderSide.Buy) ? TradeDirection.Long : TradeDirection.Short,
(TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum<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(), "");
}
@@ -217,7 +170,8 @@ namespace Managing.Infrastructure.Exchanges.Helpers
return null;
return new Trade(data.CreateTime, TradeDirection.None,
(TradeStatus)data.Status, (TradeType)data.Type, MiscExtensions.ParseEnum<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, "");
}
@@ -280,7 +234,7 @@ namespace Managing.Infrastructure.Exchanges.Helpers
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
{
Name = $"{GetExplorerUrl(trader.Address)}",
Value = $"Avg Win / Avg Loss / Winrate / ROI \n {trader.AverageWin:#.##}$ / {trader.AverageLoss:#.##}$ / {trader.Winrate}% / {Convert.ToDecimal(trader.Roi) * 100:#.##}%",
Value =
$"Avg Win / Avg Loss / Winrate / ROI \n {trader.AverageWin:#.##}$ / {trader.AverageLoss:#.##}$ / {trader.Winrate}% / {Convert.ToDecimal(trader.Roi) * 100:#.##}%",
});
}
@@ -31,6 +32,48 @@ public static class DiscordHelpers
return embed;
}
public static Embed GetFundingRateEmbed(FundingRate fundingRate, string title, FundingRate? oldRate = null)
{
var fields = new List<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)
{
return new EmbedBuilder
@@ -57,14 +100,16 @@ public static class DiscordHelpers
fields.Add(new EmbedFieldBuilder
{
Name = $"{GetExplorerUrl(trade.ExchangeOrderId)}",
Value = $"Side / Ticker / Open / Qty / Leverage / LiqPrice \n {trade.Direction} / {trade.Ticker} / {trade.Price:#.##}$ / {trade.Quantity:#.##}$ / x{trade.Leverage:#.##} / {Convert.ToDecimal(trade.Message):#.##}$",
Value =
$"Side / Ticker / Open / Qty / Leverage / LiqPrice \n {trade.Direction} / {trade.Ticker} / {trade.Price:#.##}$ / {trade.Quantity:#.##}$ / x{trade.Leverage:#.##} / {Convert.ToDecimal(trade.Message):#.##}$",
});
}
fields.Add(new EmbedFieldBuilder
{
Name = "Summary",
Value = $"Long / Short / \n {trades.Count(t => t.Direction == Common.Enums.TradeDirection.Long)} / {trades.Count(t => t.Direction == Common.Enums.TradeDirection.Short)}"
Value =
$"Long / Short / \n {trades.Count(t => t.Direction == Common.Enums.TradeDirection.Long)} / {trades.Count(t => t.Direction == Common.Enums.TradeDirection.Short)}"
});
var embed = new EmbedBuilder
@@ -77,4 +122,34 @@ public static class DiscordHelpers
return embed;
}
}
public static Embed GetFundingRatesEmbed(List<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
// The hosted service has started
public async Task StartAsync(CancellationToken cancellationToken)
{
@@ -85,7 +86,9 @@ namespace Managing.Infrastructure.Messengers.Discord
case Constants.DiscordSlashCommand.LeaderboardPosition:
await SlashCommands.HandleLeadboardPositionCommand(_services, command);
break;
case Constants.DiscordSlashCommand.FundingRates:
await SlashCommands.HandleFundingRateCommand(_services, command);
break;
}
}
@@ -117,6 +120,10 @@ namespace Managing.Infrastructure.Messengers.Discord
noobiesboardCommand.WithDescription("Shows the last Noobies board");
applicationCommandProperties.Add(noobiesboardCommand.Build());
var fundingRatesCommand = new SlashCommandBuilder();
fundingRatesCommand.WithName(Constants.DiscordSlashCommand.FundingRates);
fundingRatesCommand.WithDescription("Shows the last funding rates");
applicationCommandProperties.Add(fundingRatesCommand.Build());
await _client.BulkOverwriteGlobalApplicationCommandsAsync(applicationCommandProperties.ToArray());
}
@@ -131,7 +138,6 @@ namespace Managing.Infrastructure.Messengers.Discord
{
List<ApplicationCommandProperties> commands = new();
return commands;
}
@@ -139,10 +145,7 @@ namespace Managing.Infrastructure.Messengers.Discord
// logging
private async Task Log(LogMessage arg)
{
await Task.Run(() =>
{
_logger.LogInformation(arg.ToString());
});
await Task.Run(() => { _logger.LogInformation(arg.ToString()); });
}
private async Task CommandExecuted(Optional<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}"));
return;
}
// react to message
await context.Message.AddReactionAsync(new Emoji("🤖")); // robot emoji
}
@@ -177,13 +181,16 @@ namespace Managing.Infrastructure.Messengers.Discord
_client.ButtonExecuted -= ButtonHandler;
_client.SlashCommandExecuted -= SlashCommandHandler;
}
public void Dispose()
{
_client?.Dispose();
}
#endregion
#region In
public async Task ButtonHandler(SocketMessageComponent component)
{
var parameters = component.Data.CustomId.Split(new[] { _separator }, StringSplitOptions.None);
@@ -191,7 +198,8 @@ namespace Managing.Infrastructure.Messengers.Discord
if (component.User.GlobalName != "crypto_saitama")
{
await component.Channel.SendMessageAsync("Sorry bro, this feature is not accessible for you.. Do not hesitate to send me approx. 456 121 $ and i give you full access");
await component.Channel.SendMessageAsync(
"Sorry bro, this feature is not accessible for you.. Do not hesitate to send me approx. 456 121 $ and i give you full access");
}
else
{
@@ -219,7 +227,10 @@ namespace Managing.Infrastructure.Messengers.Discord
var json = MiscExtensions.Base64Decode(parameters[1]);
var trade = JsonConvert.DeserializeObject<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)
@@ -234,19 +245,24 @@ namespace Managing.Infrastructure.Messengers.Discord
var moneyManagementName = parameters[5];
var expiration = DateTime.Parse(parameters[6]);
await OpenPosition(component, accountName, moneyManagementName, PositionInitiator.User, ticker, direction, timeframe, expiration, false);
await OpenPosition(component, accountName, moneyManagementName, PositionInitiator.User, ticker, direction,
timeframe, expiration, false);
}
private async Task OpenPosition(SocketMessageComponent component, string accountName, string moneyManagement, PositionInitiator initiator, Ticker ticker, TradeDirection direction, Timeframe timeframe, DateTime expiration, bool ignoreSLTP, decimal? leverage = null)
private async Task OpenPosition(SocketMessageComponent component, string accountName, string moneyManagement,
PositionInitiator initiator, Ticker ticker, TradeDirection direction, Timeframe timeframe,
DateTime expiration, bool ignoreSLTP, decimal? leverage = null)
{
if (DateTime.Now > expiration)
{
await component.Channel.SendMessageAsync("Sorry I can't open position because you tried to click on a expired button.");
await component.Channel.SendMessageAsync(
"Sorry I can't open position because you tried to click on a expired button.");
}
else
{
var exchangeService = (IExchangeService)_services.GetService(typeof(IExchangeService));
var moneyManagementService = (IMoneyManagementService)_services.GetService(typeof(IMoneyManagementService));
var moneyManagementService =
(IMoneyManagementService)_services.GetService(typeof(IMoneyManagementService));
var accountService = (IAccountService)_services.GetService(typeof(IAccountService));
var tradingService = (ITradingService)_services.GetService(typeof(ITradingService));
@@ -261,7 +277,8 @@ namespace Managing.Infrastructure.Messengers.Discord
var position = await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
.Handle(tradeCommand);
var builder = new ComponentBuilder().WithButton("Close Position", $"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Identifier}");
var builder = new ComponentBuilder().WithButton("Close Position",
$"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Identifier}");
await component.Channel.SendMessageAsync(MessengerHelpers.GetPositionMessage(position),
components: builder.Build());
@@ -272,10 +289,10 @@ namespace Managing.Infrastructure.Messengers.Discord
private string GetClosingPositionMessage(Position position)
{
return $"Closing : {position.OriginDirection} {position.Open.Ticker} \n" +
$"Open Price : {position.Open.Price} \n" +
$"Closing Price : {position.Open.Price} \n" +
$"Quantity :{position.Open.Quantity} \n" +
$"PNL : {position.ProfitAndLoss.Net} $";
$"Open Price : {position.Open.Price} \n" +
$"Closing Price : {position.Open.Price} \n" +
$"Quantity :{position.Open.Quantity} \n" +
$"PNL : {position.ProfitAndLoss.Net} $";
}
private async Task ClosePosition(SocketMessageComponent component, string[] parameters)
@@ -287,42 +304,45 @@ namespace Managing.Infrastructure.Messengers.Discord
await component.RespondAsync("Alright, let met few seconds to close this position");
var position = _tradingService.GetPositionByIdentifier(parameters[1]);
var command = new ClosePositionCommand(position);
var result = await new ClosePositionCommandHandler(exchangeService, accountService, tradingService).Handle(command);
var result =
await new ClosePositionCommandHandler(exchangeService, accountService, tradingService).Handle(command);
var fields = new List<EmbedFieldBuilder>()
{
new EmbedFieldBuilder
{
Name = "Direction",
Value = position.OriginDirection,
IsInline = true
},
new EmbedFieldBuilder
{
Name = "Open Price",
Value = $"{position.Open.Price:#.##}",
IsInline = true
},
new EmbedFieldBuilder
{
Name = "Quantity",
Value = $"{position.Open.Quantity:#.##}",
IsInline = true
},
new EmbedFieldBuilder
{
Name = "Pnl",
Value = $"{position.ProfitAndLoss.Net:#.##}",
IsInline = true
},
new EmbedFieldBuilder
{
Name = "Direction",
Value = position.OriginDirection,
IsInline = true
},
new EmbedFieldBuilder
{
Name = "Open Price",
Value = $"{position.Open.Price:#.##}",
IsInline = true
},
new EmbedFieldBuilder
{
Name = "Quantity",
Value = $"{position.Open.Quantity:#.##}",
IsInline = true
},
new EmbedFieldBuilder
{
Name = "Pnl",
Value = $"{position.ProfitAndLoss.Net:#.##}",
IsInline = true
},
};
var embed = DiscordHelpers.GetEmbed(position.AccountName, $"Position status is now {result.Status}", fields, position.ProfitAndLoss.Net > 0 ? Color.Green : Color.Red);
var embed = DiscordHelpers.GetEmbed(position.AccountName, $"Position status is now {result.Status}", fields,
position.ProfitAndLoss.Net > 0 ? Color.Green : Color.Red);
await component.Channel.SendMessageAsync("", embed: embed);
}
#endregion
#region Out
public async Task SendSignal(string message)
{
var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel;
@@ -330,18 +350,21 @@ namespace Managing.Infrastructure.Messengers.Discord
await channel.SendMessageAsync(message, components: builder.Build());
}
public async Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, Timeframe timeframe)
public async Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction,
Timeframe timeframe)
{
var expirationDate = DateTime.Now.AddMinutes(_settings.ButtonExpirationMinutes).ToString("G");
var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel;
var builder = new ComponentBuilder().WithButton("Open Position", $"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}");
var builder = new ComponentBuilder().WithButton("Open Position",
$"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}");
await channel.SendMessageAsync(message, components: builder.Build());
}
public async Task SendIncreasePosition(string address, Trade trade, string copyAccountName, Trade? oldTrade = null)
public async Task SendIncreasePosition(string address, Trade trade, string copyAccountName,
Trade? oldTrade = null)
{
var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel;
var fields = new List<EmbedFieldBuilder>()
{
@@ -373,7 +396,10 @@ namespace Managing.Infrastructure.Messengers.Discord
if (oldTrade != null)
{
fields.Add(new EmbedFieldBuilder { Name = "Increasy by", Value = $"{(trade.Quantity - oldTrade.Quantity) / trade.Leverage:#.##} $" });
fields.Add(new EmbedFieldBuilder
{
Name = "Increasy by", Value = $"{(trade.Quantity - oldTrade.Quantity) / trade.Leverage:#.##} $"
});
}
var titlePrefix = oldTrade != null ? "Increase " : "";
@@ -398,24 +424,29 @@ namespace Managing.Infrastructure.Messengers.Discord
if (oldTrade == null)
{
builder.WithButton($"Copy with {mm.Name}", $"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}");
builder.WithButton($"Copy with {mm.Name}",
$"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}");
}
else
{
builder.WithButton($"Increase with {mm.Name}", $"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}");
builder.WithButton($"Increase with {mm.Name}",
$"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}");
}
}
var embed = DiscordHelpers.GetEmbed(address, $"{titlePrefix}{trade.Direction} {trade.Ticker}", fields, trade.Direction == TradeDirection.Long ? Color.Green : Color.Red);
var embed = DiscordHelpers.GetEmbed(address, $"{titlePrefix}{trade.Direction} {trade.Ticker}", fields,
trade.Direction == TradeDirection.Long ? Color.Green : Color.Red);
await channel.SendMessageAsync("", components: builder.Build(), embed: embed);
}
public async Task SendPosition(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, Timeframe timeframe)
public async Task SendPosition(string message, TradingExchanges exchange, Ticker ticker,
TradeDirection direction, Timeframe timeframe)
{
var expirationDate = DateTime.Now.AddMinutes(_settings.ButtonExpirationMinutes).ToString("G");
var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel;
var builder = new ComponentBuilder().WithButton("Open Position", $"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}");
var builder = new ComponentBuilder().WithButton("Open Position",
$"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}");
await channel.SendMessageAsync(message, components: builder.Build());
}
@@ -433,7 +464,9 @@ namespace Managing.Infrastructure.Messengers.Discord
public async Task SendTradeMessage(string message, bool isBadBehavior = false)
{
var channel = _client.GetChannel(isBadBehavior ? _settings.TroublesChannelId : _settings.TradesChannelId) as IMessageChannel;
var channel =
_client.GetChannel(isBadBehavior ? _settings.TroublesChannelId : _settings.TradesChannelId) as
IMessageChannel;
await channel.SendMessageAsync(message);
}
@@ -467,7 +500,8 @@ namespace Managing.Infrastructure.Messengers.Discord
}
};
var embed = DiscordHelpers.GetEmbed(address, $"Closed {oldTrade.Direction} {oldTrade.Ticker}", fields, oldTrade.Direction == TradeDirection.Long ? Color.DarkGreen : Color.DarkRed);
var embed = DiscordHelpers.GetEmbed(address, $"Closed {oldTrade.Direction} {oldTrade.Ticker}", fields,
oldTrade.Direction == TradeDirection.Long ? Color.DarkGreen : Color.DarkRed);
var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel;
await channel.SendMessageAsync("", embed: embed);
}
@@ -508,7 +542,8 @@ namespace Managing.Infrastructure.Messengers.Discord
}
};
var embed = DiscordHelpers.GetEmbed(address, $"Decrease {trade.Direction} {trade.Ticker}", fields, Color.Blue);
var embed = DiscordHelpers.GetEmbed(address, $"Decrease {trade.Direction} {trade.Ticker}", fields,
Color.Blue);
var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel;
await channel.SendMessageAsync("", embed: embed);
}
@@ -517,7 +552,8 @@ namespace Managing.Infrastructure.Messengers.Discord
public async Task SendPosition(Position position)
{
var channel = _client.GetChannel(_settings.TradesChannelId) as IMessageChannel;
var builder = new ComponentBuilder().WithButton("Close Position", $"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Open.ExchangeOrderId}");
var builder = new ComponentBuilder().WithButton("Close Position",
$"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Open.ExchangeOrderId}");
await channel.SendMessageAsync(MessengerHelpers.GetPositionMessage(position), components: builder.Build());
}
@@ -525,7 +561,6 @@ namespace Managing.Infrastructure.Messengers.Discord
{
var channel = _client.GetChannel(_settings.LeaderboardChannelId) as IMessageChannel;
await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Leaderboard"));
}
public async Task SendBadTraders(List<Trader> traders)
@@ -534,24 +569,37 @@ namespace Managing.Infrastructure.Messengers.Discord
await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Noobiesboard"));
}
public async Task SendDowngradedFundingRate(FundingRate fundingRate)
{
var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel;
await channel.SendMessageAsync("",
embed: DiscordHelpers.GetFundingRateEmbed(fundingRate, "Funding rate new opportunity"));
}
public Task SendNewTopFundingRate(FundingRate newRate)
{
var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel;
return channel.SendMessageAsync("",
embed: DiscordHelpers.GetFundingRateEmbed(newRate, "Funding rate new opportunity"));
}
public Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate)
{
var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel;
return channel.SendMessageAsync("",
embed: DiscordHelpers.GetFundingRateEmbed(newRate, "Funding rate new opportunity", oldRate));
}
#endregion
public class CopyTradeData
{
[JsonProperty(PropertyName = "D")]
public TradeDirection Direction { get; set; }
[JsonProperty(PropertyName = "T")]
public Ticker Ticker { get; set; }
[JsonProperty(PropertyName = "A")]
public string AccountName { get; set; }
[JsonProperty(PropertyName = "E")]
public int ExpirationMinute { get; set; }
[JsonProperty(PropertyName = "L")]
public decimal Leverage { get; set; }
[JsonProperty(PropertyName = "M")]
public string MoneyManagementName { get; internal set; }
[JsonProperty(PropertyName = "D")] public TradeDirection Direction { get; set; }
[JsonProperty(PropertyName = "T")] public Ticker Ticker { get; set; }
[JsonProperty(PropertyName = "A")] public string AccountName { get; set; }
[JsonProperty(PropertyName = "E")] public int ExpirationMinute { get; set; }
[JsonProperty(PropertyName = "L")] public decimal Leverage { get; set; }
[JsonProperty(PropertyName = "M")] public string MoneyManagementName { get; internal set; }
}
}
}
}

View File

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

View File

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

View File

@@ -6,8 +6,15 @@ namespace Managing.Infrastructure.Tests;
public class TradaoTests
{
private readonly TradaoService _tradaoService;
public TradaoTests()
{
_tradaoService = new TradaoService();
}
[Fact]
public async void Should_return_best_trader()
public async void Should_return_best_trader()
{
var service = new TradaoService();
var details = await service.GetBestTrader();
@@ -21,4 +28,39 @@ public class TradaoTests
var details = (await service.GetBadTrader()).FindBadTrader();
Assert.NotNull(details);
}
}
[Fact]
public async void Should_return_funding_rates()
{
var service = new TradaoService();
var details = await service.GetFundingRates();
Assert.NotNull(details);
}
[Fact]
public void GetApy_WithPositiveRate_ReturnsExpectedPositiveApy()
{
var result = _tradaoService.GetApy("0.0055");
Assert.True(result > 48m);
}
[Fact]
public void GetApy_WithNegativeRate_ReturnsPositiveApy()
{
var result = _tradaoService.GetApy("-0.00834530");
Assert.True(result < -73m);
}
[Fact]
public void GetApy_WithZeroRate_ReturnsZeroApy()
{
var result = _tradaoService.GetApy("0");
Assert.Equal(0m, result);
}
[Fact]
public void GetApy_WithInvalidFormat_ThrowsFormatException()
{
Assert.Throws<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.Statistics;
using Managing.Domain.Trades;
using Managing.Infrastructure.Evm.Abstractions;
using Managing.Infrastructure.Evm.Models.Gmx;
using Managing.Infrastructure.Evm.Referentials;
using Managing.Infrastructure.Evm.Services;
using Managing.Infrastructure.Evm.Services.Gmx;
using NBitcoin;
using Nethereum.Contracts;
using Nethereum.Contracts.Standards.ERC20.ContractDefinition;
using Nethereum.HdWallet;
using Nethereum.Hex.HexTypes;
using Nethereum.Signer;
using Nethereum.Web3;
using Nethereum.HdWallet;
using System.Numerics;
using System.Net.Http.Json;
using Managing.Infrastructure.Evm.Services;
using Managing.Domain.Candles;
using Managing.Infrastructure.Evm.Abstractions;
using Managing.Core;
using static Managing.Common.Enums;
using Managing.Infrastructure.Evm.Services.Gmx;
using Nethereum.Contracts.Standards.ERC20.ContractDefinition;
using Managing.Common;
using Managing.Domain.Trades;
using Managing.Domain.Accounts;
using Managing.Infrastructure.Evm.Models.Gmx;
using Managing.Infrastructure.Evm.Referentials;
namespace Managing.Infrastructure.Evm;
@@ -590,6 +591,15 @@ public class EvmManager : IEvmManager
return await GmxService.GetTrade(web3, reference, ticker);
}
public Task<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)
{
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.Trades;
using Managing.Infrastructure.Evm.Models;
using System.Net.Http.Json;
namespace Managing.Infrastructure.Evm.Services;
@@ -10,14 +12,32 @@ public class TradaoService : ITradaoService
{
private readonly HttpClient _httpClient;
private readonly HashSet<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()
{
_httpClient = new HttpClient(); ;
_httpClient = new HttpClient();
;
}
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)
{
@@ -30,7 +50,8 @@ public class TradaoService : ITradaoService
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)
{
@@ -42,7 +63,9 @@ public class TradaoService : ITradaoService
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>();
@@ -60,7 +83,7 @@ public class TradaoService : ITradaoService
Convert.ToDecimal(position.averagePrice),
Convert.ToDecimal(position.position) / Convert.ToDecimal(position.collateral),
address, position.liqPrice
);
);
trades.Add(trade);
}
@@ -68,12 +91,15 @@ public class TradaoService : ITradaoService
return trades;
}
private async Task<List<Trader>> GetTraderDetails(TradaoList traders)
{
var result = new List<Trader>();
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)
result.Add(Map(response, trader.user));
@@ -95,4 +121,94 @@ public class TradaoService : ITradaoService
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; }
}
}