From c25752c67016a5bff39a3fb2ff7752d3061e4d6b Mon Sep 17 00:00:00 2001 From: cryptooda Date: Thu, 20 Jun 2024 22:38:26 +0700 Subject: [PATCH] Add backup management --- assets/Todo-v2.md | 1 + src/Managing.Api/Managing.Api.csproj | 3 + src/Managing.Api/Program.cs | 5 +- .../Repositories/IBotRepository.cs | 8 ++ .../Abstractions/IBotService.cs | 4 + src/Managing.Application/Bots/SimpleBot.cs | 13 +- src/Managing.Application/Bots/TradingBot.cs | 46 +++++++ .../ManageBot/BackupBotCommandHandler.cs | 68 ++++++++++ .../ManageBot/BotService.cs | 37 +++++ .../ManageBot/LoadBackupBotCommandHandler.cs | 127 ++++++++++++++++++ src/Managing.Bootstrap/ApiBootstrap.cs | 3 + src/Managing.Common/Enums.cs | 3 +- src/Managing.Domain/Bots/Bot.cs | 7 +- src/Managing.Domain/Bots/BotBackup.cs | 9 ++ src/Managing.Domain/Bots/IBot.cs | 2 + .../BotRepository.cs | 41 ++++++ .../MongoDb/Collections/BotDto.cs | 13 ++ .../MongoDb/MongoMappers.cs | 28 +++- 18 files changed, 413 insertions(+), 5 deletions(-) create mode 100644 src/Managing.Application.Abstractions/Repositories/IBotRepository.cs create mode 100644 src/Managing.Application/Abstractions/IBotService.cs create mode 100644 src/Managing.Application/ManageBot/BackupBotCommandHandler.cs create mode 100644 src/Managing.Application/ManageBot/BotService.cs create mode 100644 src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs create mode 100644 src/Managing.Domain/Bots/BotBackup.cs create mode 100644 src/Managing.Infrastructure.Database/BotRepository.cs create mode 100644 src/Managing.Infrastructure.Database/MongoDb/Collections/BotDto.cs diff --git a/assets/Todo-v2.md b/assets/Todo-v2.md index 8f4c220..f752975 100644 --- a/assets/Todo-v2.md +++ b/assets/Todo-v2.md @@ -151,6 +151,7 @@ ________________________________________________________________________________ - [ ] Create method to update the money management use by the bot - [ ] Implement from/to tickers array pattern - [ ] Extract all managing trade method into a TradingBox class => Create composable trading bot type easily +- [ ] Bot backup worker: Every x, get saved bots and check if still running. If not running call api to reboot bot. # Front-end ## Improve Account page diff --git a/src/Managing.Api/Managing.Api.csproj b/src/Managing.Api/Managing.Api.csproj index be05f0e..843053a 100644 --- a/src/Managing.Api/Managing.Api.csproj +++ b/src/Managing.Api/Managing.Api.csproj @@ -36,6 +36,9 @@ Always + + Always + Always diff --git a/src/Managing.Api/Program.cs b/src/Managing.Api/Program.cs index 7d8eba9..e01d702 100644 --- a/src/Managing.Api/Program.cs +++ b/src/Managing.Api/Program.cs @@ -17,12 +17,15 @@ using Serilog.Sinks.Elasticsearch; // Builder var builder = WebApplication.CreateBuilder(args); -builder.Configuration.AddJsonFile("appsettings.Lowpro.json", optional: true, reloadOnChange: true) +builder.Configuration.SetBasePath(System.AppContext.BaseDirectory); +builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json") .AddJsonFile($"config.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true); builder.Configuration.AddEnvironmentVariables(); builder.Configuration.AddUserSecrets(); + builder.Host.UseSerilog((hostBuilder, loggerConfiguration) => { var envName = builder.Environment.EnvironmentName.ToLower().Replace(".", "-"); diff --git a/src/Managing.Application.Abstractions/Repositories/IBotRepository.cs b/src/Managing.Application.Abstractions/Repositories/IBotRepository.cs new file mode 100644 index 0000000..0473039 --- /dev/null +++ b/src/Managing.Application.Abstractions/Repositories/IBotRepository.cs @@ -0,0 +1,8 @@ +using Managing.Domain.Bots; + +public interface IBotRepository +{ + Task InsertBotAsync(BotBackup bot); + IEnumerable GetBots(); + Task UpdateBackupBot(BotBackup bot); +} diff --git a/src/Managing.Application/Abstractions/IBotService.cs b/src/Managing.Application/Abstractions/IBotService.cs new file mode 100644 index 0000000..95feed0 --- /dev/null +++ b/src/Managing.Application/Abstractions/IBotService.cs @@ -0,0 +1,4 @@ + public interface IBotService + { + void SaveBotBackup(BotBackup botBackup); + } \ No newline at end of file diff --git a/src/Managing.Application/Bots/SimpleBot.cs b/src/Managing.Application/Bots/SimpleBot.cs index ee8feb8..8ed092a 100644 --- a/src/Managing.Application/Bots/SimpleBot.cs +++ b/src/Managing.Application/Bots/SimpleBot.cs @@ -1,13 +1,14 @@ using Managing.Domain.Bots; using Managing.Domain.Workflows; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; namespace Managing.Application.Bots { public class SimpleBot : Bot { public readonly ILogger Logger; - private readonly Workflow _workflow; + private Workflow _workflow; public SimpleBot(string name, ILogger logger, Workflow workflow) : base(name) { @@ -35,5 +36,15 @@ namespace Managing.Application.Bots Logger.LogInformation("__________________________________________________"); }); } + + public override string GetBackup() + { + return JsonConvert.SerializeObject(_workflow); + } + + public override void LoadBackup(BotBackup backup) + { + _workflow = JsonConvert.DeserializeObject(backup.Data); + } } } diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index 1dde4ab..82fb14b 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -11,6 +11,7 @@ using Managing.Domain.Shared.Helpers; using Managing.Domain.Strategies; using Managing.Domain.Trades; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using static Managing.Common.Enums; namespace Managing.Application.Bots; @@ -64,6 +65,7 @@ public class TradingBot : Bot, ITradingBot MessengerService = messengerService; TradingService = tradingService; + IsForWatchingOnly = isForWatchingOnly; FlipPosition = flipPosition; AccountName = accountName; @@ -648,4 +650,48 @@ public class TradingBot : Bot, ITradingBot await MessengerService.SendTradeMessage(message, isBadBehavior); } } + + public override string GetBackup() + { + return JsonConvert.SerializeObject(new TradingBotBackup + { + Name = Name, + BotType = BotType, + Strategies = Strategies, + Signals = Signals, + Positions = Positions, + Timeframe = Timeframe, + Ticker = Ticker, + Scenario = Scenario, + AccountName = AccountName, + IsForWatchingOnly = IsForWatchingOnly, + WalletBalances = WalletBalances, + MoneyManagement = MoneyManagement + }); + } + + public override void LoadBackup(BotBackup backup) + { + var data = JsonConvert.DeserializeObject(backup.Data); + Strategies = data.Strategies; + Signals = data.Signals; + Positions = data.Positions; + WalletBalances = data.WalletBalances; + } } + +public class TradingBotBackup +{ + public string Name { get; set; } + public BotType BotType { get; set; } + public HashSet Strategies { get; set; } + public HashSet Signals { get; set; } + public List Positions { get; set; } + public Timeframe Timeframe { get; set; } + public Ticker Ticker { get; set; } + public string Scenario { get; set; } + public string AccountName { get; set; } + public bool IsForWatchingOnly { get; set; } + public Dictionary WalletBalances { get; set; } + public MoneyManagement MoneyManagement { get; internal set; } +} \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/BackupBotCommandHandler.cs b/src/Managing.Application/ManageBot/BackupBotCommandHandler.cs new file mode 100644 index 0000000..4b5d127 --- /dev/null +++ b/src/Managing.Application/ManageBot/BackupBotCommandHandler.cs @@ -0,0 +1,68 @@ +using Managing.Domain.Bots; +using MediatR; +using static Managing.Common.Enums; +using Managing.Application.Abstractions; +using Managing.Application.ManageBot.Commands; +using Managing.Domain.MoneyManagements; + +namespace Managing.Application.ManageBot +{ + public class BackupBotCommandHandler : IRequestHandler + { + private readonly IBotFactory _botFactory; + private readonly ITaskCache _taskCache; + private readonly IMoneyManagementService _moneyManagementService; + private readonly IBotService _botService; + + public BackupBotCommandHandler(IBotFactory botFactory, ITaskCache taskCache, IBotService botService) + { + _botFactory = botFactory; + _taskCache = taskCache; + _botService = botService; + } + + public Task Handle(BackupBotCommand request, CancellationToken cancellationToken) + { + var botBackup = new BotBackup + { + Name = request.Name, + BotType = request.BotType, + Data = "" + }; + + switch (request.BotType) + { + case BotType.SimpleBot: + var simpleBot = _taskCache.Get(request.Name); + botBackup.Data = simpleBot.GetBackup(); + break; + case BotType.ScalpingBot: + var scalpingBot = _taskCache.Get(request.Name); + botBackup.Data = scalpingBot.GetBackup(); + break; + case BotType.FlippingBot: + var flippingBot = _taskCache.Get(request.Name); + botBackup.Data = flippingBot.GetBackup(); + break; + default: + return Task.FromResult(false); + } + + _botService.SaveBotBackup(botBackup); + + return Task.FromResult(true); + } + } + + public class BackupBotCommand : IRequest + { + public string Name { get; } + public BotType BotType { get; } + + public BackupBotCommand(BotType botType, string name) + { + BotType = botType; + Name = name; + } + } +} diff --git a/src/Managing.Application/ManageBot/BotService.cs b/src/Managing.Application/ManageBot/BotService.cs new file mode 100644 index 0000000..7319cc8 --- /dev/null +++ b/src/Managing.Application/ManageBot/BotService.cs @@ -0,0 +1,37 @@ +using Managing.Application.Abstractions; +using Newtonsoft.Json; +using System.IO; + +namespace Managing.Application.ManageBot +{ + public class BotService : IBotService + { + private readonly IBotFactory _botFactory; + private readonly IBotRepository _botRepository; + + public BotService(IBotFactory botFactory, IBotRepository botRepository) + { + _botFactory = botFactory; + _botRepository = botRepository; + } + + // public void CreateBot() + // { + // // Use the factory to create a new bot + // return _botFactory.CreateBot(); + // } + + // public void LoadBotBackup(BotBackup botBackup) + // { + // // Deserialize the JSON into a Bot object + // var bot = JsonConvert.DeserializeObject(json); + + // return bot; + // } + + public async void SaveBotBackup(BotBackup botBackup) + { + await _botRepository.InsertBotAsync(botBackup); + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs b/src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs new file mode 100644 index 0000000..b6c734c --- /dev/null +++ b/src/Managing.Application/ManageBot/LoadBackupBotCommandHandler.cs @@ -0,0 +1,127 @@ +using Managing.Domain.Bots; +using MediatR; +using static Managing.Common.Enums; +using Managing.Application.Abstractions; +using Managing.Core; +using Managing.Domain.MoneyManagements; +using Newtonsoft.Json; +using Managing.Application.Bots; + +namespace Managing.Application.ManageBot +{ + public class LoadBackupBotCommandHandler : IRequestHandler + { + private readonly IBotFactory _botFactory; + private readonly ITaskCache _taskCache; + private readonly IMoneyManagementService _moneyManagementService; + private readonly IBotRepository _botRepository; + private readonly IMediator _mediator; + + public LoadBackupBotCommandHandler( + IBotFactory botFactory, + ITaskCache taskCache, + IMoneyManagementService moneyManagementService, + IBotRepository botRepository, + IMediator mediator) + { + _botFactory = botFactory; + _taskCache = taskCache; + _moneyManagementService = moneyManagementService; + _botRepository = botRepository; + _mediator = mediator; + } + + public Task Handle(LoadBackupBotCommand request, CancellationToken cancellationToken) + { + BotStatus botStatus = BotStatus.Down; + var backupBots = _botRepository.GetBots(); + var result = new Dictionary(); + + foreach (var backupBot in backupBots) + { + // Check if bot is existing in cache + switch (backupBot.BotType) + { + case BotType.SimpleBot: + var simpleBot = _taskCache.Get(backupBot.Name); + if (simpleBot == null) + { + StartBot(request, backupBot); + simpleBot.LoadBackup(backupBot); + result.Add(simpleBot.GetName(), BotStatus.Backup); + } + else + { + result.Add(simpleBot.GetName(), MiscExtensions.ParseEnum(simpleBot.GetStatus())); + } + + break; + case BotType.ScalpingBot: + case BotType.FlippingBot: + var scalpingBot = _taskCache.Get(backupBot.Name); + if (scalpingBot == null) + { + StartBot(request, backupBot); + scalpingBot.LoadBackup(backupBot); + result.Add(scalpingBot.GetName(), BotStatus.Backup); + } + else + { + result.Add(scalpingBot.GetName(), MiscExtensions.ParseEnum(scalpingBot.GetStatus())); + } + break; + default: + result.Add(backupBot.Name, BotStatus.Down); + break; + } + } + + + return Task.FromResult(botStatus.ToString()); + } + + private void StartBot(LoadBackupBotCommand request, BotBackup backupBot) + { + switch (backupBot.BotType) + { + case BotType.SimpleBot: + Func> simpleBot = () => Task.FromResult(_botFactory.CreateSimpleBot(request.Name, null)); + var bot1 = _taskCache.AddOrGetExisting(request.Name, simpleBot).Result; + bot1.LoadBackup(backupBot); + break; + case BotType.ScalpingBot: + var data = JsonConvert.DeserializeObject(backupBot.Data); + Func> scalpingBot = () => Task.FromResult(_botFactory.CreateScalpingBot( + data.AccountName, + data.MoneyManagement, + data.Name, + data.Ticker, + data.Scenario, + data.Timeframe, + data.IsForWatchingOnly)); + var bot2 = _taskCache.AddOrGetExisting(request.Name, scalpingBot).Result; + bot2.LoadBackup(backupBot); + break; + case BotType.FlippingBot: + var dataFlippingBot = JsonConvert.DeserializeObject(backupBot.Data); + Func> flippingBot = () => Task.FromResult(_botFactory.CreateFlippingBot( + dataFlippingBot.AccountName, + dataFlippingBot.MoneyManagement, + dataFlippingBot.Name, + dataFlippingBot.Ticker, + dataFlippingBot.Scenario, + dataFlippingBot.Timeframe, + dataFlippingBot.IsForWatchingOnly)); + var bot3 = _taskCache.AddOrGetExisting(request.Name, flippingBot).Result; + bot3.LoadBackup(backupBot); + break; + }; + } + } + + public class LoadBackupBotCommand : IRequest + { + public string Name { get; internal set; } + public string AccountName { get; internal set; } + } +} diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index f90c98a..0dfbc24 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -41,6 +41,7 @@ using Binance.Net.Interfaces.Clients; using Managing.Infrastructure.Evm.Services; using Managing.Application.Workflows; using Managing.Application.Bots.Base; +using Managing.Application.ManageBot; namespace Managing.Bootstrap; @@ -105,6 +106,7 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Cache services.AddDistributedMemoryCache(); @@ -123,6 +125,7 @@ public static class ApiBootstrap services.AddTransient(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Stream services.AddSingleton(); diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index 6bf4d83..84c7988 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -73,7 +73,8 @@ public static class Enums { Down, Starting, - Up + Up, + Backup } public enum SignalStatus diff --git a/src/Managing.Domain/Bots/Bot.cs b/src/Managing.Domain/Bots/Bot.cs index ca4af2a..7e3959d 100644 --- a/src/Managing.Domain/Bots/Bot.cs +++ b/src/Managing.Domain/Bots/Bot.cs @@ -1,4 +1,5 @@ -using static Managing.Common.Enums; +using System.Text.Json; +using static Managing.Common.Enums; namespace Managing.Domain.Bots { @@ -14,6 +15,7 @@ namespace Managing.Domain.Bots public int Interval { get; set; } public BotStatus Status { get; set; } private CancellationTokenSource CancellationToken { get; set; } + public string Data { get; set; } public Bot(string name) { @@ -71,5 +73,8 @@ namespace Managing.Domain.Bots { return Name; } + + public abstract string GetBackup(); + public abstract void LoadBackup(BotBackup backup); } } \ No newline at end of file diff --git a/src/Managing.Domain/Bots/BotBackup.cs b/src/Managing.Domain/Bots/BotBackup.cs new file mode 100644 index 0000000..5ea153c --- /dev/null +++ b/src/Managing.Domain/Bots/BotBackup.cs @@ -0,0 +1,9 @@ +using Managing.Domain.MoneyManagements; +using static Managing.Common.Enums; + +public class BotBackup +{ + public string Name { get; set; } + public BotType BotType { get; set; } + public string Data { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Domain/Bots/IBot.cs b/src/Managing.Domain/Bots/IBot.cs index 7572a3d..9c2c30f 100644 --- a/src/Managing.Domain/Bots/IBot.cs +++ b/src/Managing.Domain/Bots/IBot.cs @@ -8,5 +8,7 @@ void Restart(); string GetStatus(); string GetName(); + string GetBackup(); + void LoadBackup(BotBackup backup); } } diff --git a/src/Managing.Infrastructure.Database/BotRepository.cs b/src/Managing.Infrastructure.Database/BotRepository.cs new file mode 100644 index 0000000..4147431 --- /dev/null +++ b/src/Managing.Infrastructure.Database/BotRepository.cs @@ -0,0 +1,41 @@ +using Managing.Domain.Bots; +using Managing.Infrastructure.Databases.MongoDb; +using Managing.Infrastructure.Databases.MongoDb.Abstractions; +using Managing.Infrastructure.Databases.MongoDb.Collections; + +namespace Managing.Infrastructure.Databases; + +public class BotRepository : IBotRepository +{ + private readonly IMongoRepository _botRepository; + + public BotRepository(IMongoRepository botRepository) + { + _botRepository = botRepository; + } + + public async Task InsertBotAsync(BotBackup bot) + { + await _botRepository.InsertOneAsync(MongoMappers.Map(bot)); + } + + public IEnumerable GetBots() + { + var bots = _botRepository.FindAll(); + return bots.Select(b => MongoMappers.Map(b)); + } + + public async Task UpdateBackupBot(BotBackup bot) + { + var b = _botRepository.FindOne(b => b.Name == bot.Name); + var dto = MongoMappers.Map(bot); + dto.Id = b.Id; + _botRepository.Update(dto); + } + + public void DeleteBotByName(string name) + { + var bot = _botRepository.FindOne(b => b.Name == name); + _botRepository.DeleteById(bot.Id.ToString()); + } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/Collections/BotDto.cs b/src/Managing.Infrastructure.Database/MongoDb/Collections/BotDto.cs new file mode 100644 index 0000000..ab7303a --- /dev/null +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/BotDto.cs @@ -0,0 +1,13 @@ +using Managing.Infrastructure.Databases.MongoDb.Attributes; +using Managing.Infrastructure.Databases.MongoDb.Configurations; +using static Managing.Common.Enums; + +namespace Managing.Infrastructure.Databases.MongoDb.Collections; + +[BsonCollection("Bots")] +public class BotDto : Document +{ + public string Name { get; set; } + public string Data { get; set; } + public BotType BotType { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs index 8412872..9a2c864 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs @@ -1,5 +1,7 @@ -using Managing.Domain.Accounts; +using System.Reflection.Metadata.Ecma335; +using Managing.Domain.Accounts; using Managing.Domain.Backtests; +using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; @@ -650,5 +652,29 @@ public static class MongoMappers }; } + internal static BotDto Map(BotBackup bot) + { + if (bot == null) return null; + + return new BotDto + { + Name = bot.Name, + BotType = bot.BotType, + Data = bot.Data, + }; + } + + internal static BotBackup Map(BotDto b) + { + if (b == null) return null; + + return new BotBackup + { + Name = b.Name, + BotType = b.BotType, + Data = b.Data + }; + } + #endregion }