* Start building with orlean

* Add missing file

* Serialize grain state

* Remove grain and proxies

* update and add plan

* Update a bit

* Fix backtest grain

* Fix backtest grain

* Clean a bit
This commit is contained in:
Oda
2025-07-30 11:03:30 +02:00
committed by GitHub
parent d281d7cd02
commit 3de8b5e00e
59 changed files with 2626 additions and 677 deletions

View File

@@ -29,7 +29,6 @@ public class BacktestController : BaseController
{
private readonly IHubContext<BacktestHub> _hubContext;
private readonly IBacktester _backtester;
private readonly IScenarioService _scenarioService;
private readonly IAccountService _accountService;
private readonly IMoneyManagementService _moneyManagementService;
private readonly IGeneticService _geneticService;
@@ -47,7 +46,6 @@ public class BacktestController : BaseController
public BacktestController(
IHubContext<BacktestHub> hubContext,
IBacktester backtester,
IScenarioService scenarioService,
IAccountService accountService,
IMoneyManagementService moneyManagementService,
IGeneticService geneticService,
@@ -55,7 +53,6 @@ public class BacktestController : BaseController
{
_hubContext = hubContext;
_backtester = backtester;
_scenarioService = scenarioService;
_accountService = accountService;
_moneyManagementService = moneyManagementService;
_geneticService = geneticService;
@@ -245,7 +242,8 @@ public class BacktestController : BaseController
return BadRequest("Sort order must be 'asc' or 'desc'");
}
var (backtests, totalCount) = await _backtester.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder);
var (backtests, totalCount) =
await _backtester.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder);
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
var response = new PaginatedBacktestsResponse
@@ -279,14 +277,14 @@ public class BacktestController : BaseController
/// <summary>
/// Runs a backtest with the specified configuration.
/// The returned backtest includes a complete TradingBotConfig that preserves all
/// settings including nullable MaxPositionTimeHours for easy bot deployment.
/// Returns a lightweight backtest result for efficient processing.
/// Use the returned ID to retrieve the full backtest data from the database.
/// </summary>
/// <param name="request">The backtest request containing configuration and parameters.</param>
/// <returns>The result of the backtest with complete configuration.</returns>
/// <returns>The lightweight result of the backtest with essential data.</returns>
[HttpPost]
[Route("Run")]
public async Task<ActionResult<Backtest>> Run([FromBody] RunBacktestRequest request)
public async Task<ActionResult<LightBacktest>> Run([FromBody] RunBacktestRequest request)
{
if (request?.Config == null)
{
@@ -310,7 +308,7 @@ public class BacktestController : BaseController
try
{
Backtest backtestResult = null;
LightBacktest backtestResult = null;
var account = await _accountService.GetAccount(request.Config.AccountName, true, false);
var user = await GetUser();
@@ -367,7 +365,9 @@ public class BacktestController : BaseController
MoneyManagement = moneyManagement,
Ticker = request.Config.Ticker,
ScenarioName = request.Config.ScenarioName,
Scenario = scenario, // Use the converted scenario object
Scenario = scenario != null
? LightScenario.FromScenario(scenario)
: null, // Convert to LightScenario for Orleans
Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly,
BotTradingBalance = request.Config.BotTradingBalance,
@@ -395,7 +395,8 @@ public class BacktestController : BaseController
request.WithCandles,
null); // No requestId for regular backtests
await NotifyBacktesingSubscriberAsync(backtestResult);
// Note: Notification is handled within the Orleans grain for LightBacktest
// The full Backtest data can be retrieved from the database using the ID if needed
return Ok(backtestResult);
}
@@ -705,17 +706,6 @@ public class BacktestController : BaseController
}
/// <summary>
/// Notifies subscribers about the backtesting results via SignalR.
/// </summary>
/// <param name="backtesting">The backtest result to notify subscribers about.</param>
private async Task NotifyBacktesingSubscriberAsync(Backtest backtesting)
{
if (backtesting != null)
{
await _hubContext.Clients.All.SendAsync("BacktestsSubscription", backtesting);
}
}
public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest)
{

View File

@@ -220,7 +220,7 @@ public class BotController : BaseController
AccountName = request.Config.AccountName,
MoneyManagement = moneyManagement,
Ticker = request.Config.Ticker,
Scenario = scenario, // Use the converted scenario object
Scenario = LightScenario.FromScenario(scenario), // Convert to LightScenario for Orleans
ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided
Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly,
@@ -782,7 +782,7 @@ public class BotController : BaseController
AccountName = request.Config.AccountName,
MoneyManagement = moneyManagement,
Ticker = request.Config.Ticker,
Scenario = scenarioForUpdate, // Use the converted scenario object
Scenario = LightScenario.FromScenario(scenarioForUpdate), // Convert to LightScenario for Orleans
ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided
Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly,

View File

@@ -16,8 +16,10 @@
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="9.0.0"/>
<PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5"/>
<PackageReference Include="Microsoft.Orleans.Core" Version="9.2.1"/>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1"/>
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7"/>
<PackageReference Include="OrleansDashboard" Version="8.2.0"/>
<PackageReference Include="Sentry.AspNetCore" Version="5.5.1"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/>
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0"/>

View File

@@ -186,6 +186,10 @@ builder.Services.AddSignalR().AddJsonProtocol();
builder.Services.AddScoped<IJwtUtils, JwtUtils>();
builder.Services.RegisterApiDependencies(builder.Configuration);
// Orleans Configuration
builder.Host.ConfigureOrleans(builder.Configuration, builder.Environment.IsProduction());
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(document =>
{

View File

@@ -39,7 +39,7 @@
"AllowedHosts": "*",
"WorkerBotManager": true,
"WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": true,
"WorkerNotifyBundleBacktest": false,
"KAIGEN_SECRET_KEY": "KaigenXCowchain",
"KAIGEN_CREDITS_ENABLED": true
"KAIGEN_CREDITS_ENABLED": false
}

View File

@@ -0,0 +1,45 @@
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Users;
using Orleans;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// Orleans grain interface for Backtest TradingBot operations.
/// This interface extends ITradingBotGrain with backtest-specific functionality.
/// </summary>
public interface IBacktestTradingBotGrain : IGrainWithGuidKey
{
/// <summary>
/// Runs a complete backtest following the exact pattern of GetBacktestingResult from Backtester.cs
/// </summary>
/// <param name="config">The trading bot configuration for this backtest</param>
/// <param name="candles">The candles to use for backtesting</param>
/// <param name="user">The user running the backtest (optional, required for saving)</param>
/// <param name="save">Whether to save the backtest results</param>
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest</param>
/// <param name="metadata">Additional metadata to associate with this backtest</param>
/// <returns>The complete backtest result</returns>
Task<LightBacktest> RunBacktestAsync(TradingBotConfig config, List<Candle> candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null);
/// <summary>
/// Gets the current backtest progress
/// </summary>
/// <returns>Backtest progress information</returns>
Task<BacktestProgress> GetBacktestProgressAsync();
}
/// <summary>
/// Represents the progress of a backtest
/// </summary>
public class BacktestProgress
{
public bool IsInitialized { get; set; }
public int TotalCandles { get; set; }
public int ProcessedCandles { get; set; }
public double ProgressPercentage { get; set; }
public bool IsComplete { get; set; }
}

View File

@@ -0,0 +1,94 @@
using Managing.Application.Abstractions.Models;
using Managing.Domain.Bots;
using Managing.Domain.Trades;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// Orleans grain interface for TradingBot operations.
/// This interface defines the distributed, async operations available for trading bots.
/// </summary>
public interface ITradingBotGrain : IGrainWithGuidKey
{
/// <summary>
/// Starts the trading bot asynchronously
/// </summary>
Task StartAsync();
/// <summary>
/// Stops the trading bot asynchronously
/// </summary>
Task StopAsync();
/// <summary>
/// Gets the current status of the trading bot
/// </summary>
Task<BotStatus> GetStatusAsync();
/// <summary>
/// Gets the current configuration of the trading bot
/// </summary>
Task<TradingBotConfig> GetConfigurationAsync();
/// <summary>
/// Updates the trading bot configuration
/// </summary>
/// <param name="newConfig">The new configuration to apply</param>
/// <returns>True if the configuration was successfully updated</returns>
Task<bool> UpdateConfigurationAsync(TradingBotConfig newConfig);
/// <summary>
/// Manually opens a position in the specified direction
/// </summary>
/// <param name="direction">The direction of the trade (Long/Short)</param>
/// <returns>The created Position object</returns>
Task<Position> OpenPositionManuallyAsync(TradeDirection direction);
/// <summary>
/// Toggles the bot between watch-only and trading mode
/// </summary>
Task ToggleIsForWatchOnlyAsync();
/// <summary>
/// Gets comprehensive bot data including positions, signals, and performance metrics
/// </summary>
Task<TradingBotResponse> GetBotDataAsync();
/// <summary>
/// Loads a bot backup into the grain state
/// </summary>
/// <param name="backup">The bot backup to load</param>
Task LoadBackupAsync(BotBackup backup);
/// <summary>
/// Forces a backup save of the current bot state
/// </summary>
Task SaveBackupAsync();
/// <summary>
/// Gets the current profit and loss for the bot
/// </summary>
Task<decimal> GetProfitAndLossAsync();
/// <summary>
/// Gets the current win rate percentage for the bot
/// </summary>
Task<int> GetWinRateAsync();
/// <summary>
/// Gets the bot's execution count (number of Run cycles completed)
/// </summary>
Task<long> GetExecutionCountAsync();
/// <summary>
/// Gets the bot's startup time
/// </summary>
Task<DateTime> GetStartupTimeAsync();
/// <summary>
/// Gets the bot's creation date
/// </summary>
Task<DateTime> GetCreateDateAsync();
}

View File

@@ -11,4 +11,8 @@
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,104 @@
using Managing.Domain.Bots;
using Managing.Domain.Trades;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Models;
/// <summary>
/// Response model for trading bot data.
/// Used to return comprehensive bot information via Orleans grains.
/// </summary>
[GenerateSerializer]
public class TradingBotResponse
{
/// <summary>
/// Bot identifier
/// </summary>
[Id(0)]
public string Identifier { get; set; } = string.Empty;
/// <summary>
/// Bot display name
/// </summary>
[Id(1)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Current bot status
/// </summary>
[Id(2)]
public BotStatus Status { get; set; }
/// <summary>
/// Bot configuration
/// </summary>
[Id(3)]
public TradingBotConfig Config { get; set; }
/// <summary>
/// Trading positions
/// </summary>
[Id(4)]
public List<Position> Positions { get; set; } = new();
/// <summary>
/// Trading signals
/// </summary>
[Id(5)]
public List<LightSignal> Signals { get; set; } = new();
/// <summary>
/// Wallet balance history
/// </summary>
[Id(6)]
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new();
/// <summary>
/// Current profit and loss
/// </summary>
[Id(7)]
public decimal ProfitAndLoss { get; set; }
/// <summary>
/// Win rate percentage
/// </summary>
[Id(8)]
public int WinRate { get; set; }
/// <summary>
/// Execution count
/// </summary>
[Id(9)]
public long ExecutionCount { get; set; }
/// <summary>
/// Startup time
/// </summary>
[Id(10)]
public DateTime StartupTime { get; set; }
/// <summary>
/// Creation date
/// </summary>
[Id(11)]
public DateTime CreateDate { get; set; }
/// <summary>
/// Current balance
/// </summary>
[Id(12)]
public decimal CurrentBalance { get; set; }
/// <summary>
/// Number of active positions
/// </summary>
[Id(13)]
public int ActivePositionsCount { get; set; }
/// <summary>
/// Last execution time
/// </summary>
[Id(14)]
public DateTime LastExecution { get; set; }
}

View File

@@ -10,6 +10,7 @@ namespace Managing.Application.Abstractions.Services
/// <summary>
/// Runs a trading bot backtest with the specified configuration and date range.
/// Automatically handles different bot types based on config.BotType.
/// Returns a LightBacktest for efficient Orleans serialization.
/// </summary>
/// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param>
/// <param name="startDate">The start date for the backtest</param>
@@ -19,8 +20,8 @@ namespace Managing.Application.Abstractions.Services
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The backtest results</returns>
Task<Backtest> RunTradingBotBacktest(
/// <returns>The lightweight backtest results</returns>
Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
@@ -33,6 +34,7 @@ namespace Managing.Application.Abstractions.Services
/// <summary>
/// Runs a trading bot backtest with pre-loaded candles.
/// Automatically handles different bot types based on config.BotType.
/// Returns a LightBacktest for efficient Orleans serialization.
/// </summary>
/// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param>
/// <param name="candles">The candles to use for backtesting</param>
@@ -40,8 +42,8 @@ namespace Managing.Application.Abstractions.Services
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The backtest results</returns>
Task<Backtest> RunTradingBotBacktest(
/// <returns>The lightweight backtest results</returns>
Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config,
List<Candle> candles,
User user = null,

View File

@@ -51,7 +51,7 @@ namespace Managing.Application.Tests
_tradingService.Object,
botService, backupBotService);
_backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
scenarioService, _accountService.Object, messengerService, kaigenService, hubContext);
scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, null);
_elapsedTimes = new List<double>();
// Initialize cross-platform file paths
@@ -78,7 +78,7 @@ namespace Managing.Application.Tests
AccountName = _account.Name,
MoneyManagement = MoneyManagement,
Ticker = ticker,
Scenario = scenario,
Scenario = LightScenario.FromScenario(scenario),
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = 1000,
@@ -128,7 +128,7 @@ namespace Managing.Application.Tests
AccountName = _account.Name,
MoneyManagement = MoneyManagement,
Ticker = ticker,
Scenario = scenario,
Scenario = LightScenario.FromScenario(scenario),
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = 1000,

View File

@@ -8,15 +8,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MathNet.Numerics" Version="5.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.5"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"/>
<PackageReference Include="Microsoft.TestPlatform.AdapterUtilities" Version="17.9.0"/>
<PackageReference Include="Moq" Version="4.20.70"/>
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1"/>
<PackageReference Include="MSTest.TestFramework" Version="3.3.1"/>
<PackageReference Include="xunit" Version="2.8.0"/>
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.TestPlatform.AdapterUtilities" Version="17.9.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -24,17 +24,17 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Api\Managing.Api.csproj"/>
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj"/>
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj"/>
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/>
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Exchanges\Managing.Infrastructure.Exchanges.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Tests\Managing.Infrastructure.Tests.csproj"/>
<ProjectReference Include="..\Managing.Api\Managing.Api.csproj" />
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj" />
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj" />
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj" />
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Exchanges\Managing.Infrastructure.Exchanges.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Tests\Managing.Infrastructure.Tests.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\"/>
<Folder Include="Data\" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,4 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Backtesting;
using Managing.Application.Bots;
@@ -8,16 +7,15 @@ using Managing.Infrastructure.Databases.InfluxDb;
using Managing.Infrastructure.Databases.InfluxDb.Models;
using Managing.Infrastructure.Evm;
using Managing.Infrastructure.Evm.Abstractions;
using Managing.Infrastructure.Evm.Models.Privy;
using Managing.Infrastructure.Evm.Services;
using Managing.Infrastructure.Evm.Subgraphs;
using Managing.Infrastructure.Exchanges;
using Managing.Infrastructure.Exchanges.Abstractions;
using Managing.Infrastructure.Exchanges.Exchanges;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Nethereum.Web3;
using static Managing.Common.Enums;
namespace Managing.Application.Tests
@@ -26,7 +24,7 @@ namespace Managing.Application.Tests
{
public static IExchangeService GetExchangeService()
{
ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
ILoggerFactory loggerFactory = new NullLoggerFactory();
var ChainlinkGmx = new ChainlinkGmx(SubgraphService.GetSubgraphClient(SubgraphProvider.ChainlinkGmx));
var Chainlink = new Chainlink(SubgraphService.GetSubgraphClient(SubgraphProvider.ChainlinkPrice));
@@ -53,23 +51,23 @@ namespace Managing.Application.Tests
exchangeProcessors);
}
public static ILogger<TradingBot> CreateTradingBotLogger()
public static ILogger<TradingBotBase> CreateTradingBotLogger()
{
ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
ILoggerFactory loggerFactory = new NullLoggerFactory();
return loggerFactory.CreateLogger<TradingBot>();
return loggerFactory.CreateLogger<TradingBotBase>();
}
public static ILogger<Backtester> CreateBacktesterLogger()
{
ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
ILoggerFactory loggerFactory = new NullLoggerFactory();
return loggerFactory.CreateLogger<Backtester>();
}
public static ILogger<CandleRepository> CreateCandleRepositoryLogger()
{
ILoggerFactory loggerFactory = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
ILoggerFactory loggerFactory = new NullLoggerFactory();
return loggerFactory.CreateLogger<CandleRepository>();
}

View File

@@ -222,17 +222,14 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
}
// Map Scenario
Scenario scenario = null;
LightScenario scenario = null;
if (runBacktestRequest.Config.Scenario != null)
{
var sReq = runBacktestRequest.Config.Scenario;
scenario = new Scenario(sReq.Name, sReq.LoopbackPeriod)
{
User = null // No user context in worker
};
scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod);
foreach (var indicatorRequest in sReq.Indicators)
{
var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
var indicator = new LightIndicator(indicatorRequest.Name, indicatorRequest.Type)
{
SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory,
@@ -244,7 +241,6 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
SmoothPeriods = indicatorRequest.SmoothPeriods,
StochPeriods = indicatorRequest.StochPeriods,
CyclePeriods = indicatorRequest.CyclePeriods,
User = null // No user context in worker
};
scenario.AddIndicator(indicator);
}

View File

@@ -7,14 +7,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj"/>
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj"/>
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj" />
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
</ItemGroup>

View File

@@ -305,7 +305,10 @@ public class StatisticService : IStatisticService
false,
false);
return backtest.Signals;
// Note: LightBacktest doesn't contain signals data, so we return an empty list
// The full signals data would need to be retrieved from the database using the backtest ID
_logger.LogWarning("GetSignals called but LightBacktest doesn't contain signals data. Returning empty list.");
return new List<LightSignal>();
}
catch (Exception ex)
{

View File

@@ -29,12 +29,6 @@ public interface IBotService
/// <returns>ITradingBot instance configured for backtesting</returns>
Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config);
// Legacy methods - these will use TradingBot internally but maintain backward compatibility
Task<ITradingBot> CreateScalpingBot(TradingBotConfig config);
Task<ITradingBot> CreateBacktestScalpingBot(TradingBotConfig config);
Task<ITradingBot> CreateFlippingBot(TradingBotConfig config);
Task<ITradingBot> CreateBacktestFlippingBot(TradingBotConfig config);
IBot CreateSimpleBot(string botName, Workflow workflow);
Task<string> StopBot(string botName);
Task<bool> DeleteBot(string botName);

View File

@@ -52,5 +52,6 @@ namespace Managing.Application.Abstractions
Task<bool> UpdateIndicatorByUser(User user, IndicatorType indicatorType, string name, int? period, int? fastPeriods,
int? slowPeriods, int? signalPeriods, double? multiplier, int? stochPeriods, int? smoothPeriods,
int? cyclePeriods);
Task<Scenario> GetScenarioByNameAndUserAsync(string scenarioName, User user);
}
}

View File

@@ -1,17 +1,13 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots;
using Managing.Application.Hubs;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
using Managing.Domain.Users;
using Managing.Domain.Workflows;
using Microsoft.AspNetCore.SignalR;
@@ -32,6 +28,7 @@ namespace Managing.Application.Backtesting
private readonly IMessengerService _messengerService;
private readonly IKaigenService _kaigenService;
private readonly IHubContext<BacktestHub> _hubContext;
private readonly IGrainFactory _grainFactory;
public Backtester(
IExchangeService exchangeService,
@@ -42,7 +39,8 @@ namespace Managing.Application.Backtesting
IAccountService accountService,
IMessengerService messengerService,
IKaigenService kaigenService,
IHubContext<BacktestHub> hubContext)
IHubContext<BacktestHub> hubContext,
IGrainFactory grainFactory)
{
_exchangeService = exchangeService;
_botFactory = botFactory;
@@ -53,6 +51,7 @@ namespace Managing.Application.Backtesting
_messengerService = messengerService;
_kaigenService = kaigenService;
_hubContext = hubContext;
_grainFactory = grainFactory;
}
public Backtest RunSimpleBotBacktest(Workflow workflow, bool save = false)
@@ -80,8 +79,8 @@ namespace Managing.Application.Backtesting
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The backtest results</returns>
public async Task<Backtest> RunTradingBotBacktest(
/// <returns>The lightweight backtest results</returns>
public async Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
@@ -114,25 +113,7 @@ namespace Managing.Application.Backtesting
try
{
var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate);
var result = await RunBacktestWithCandles(config, candles, user, withCandles, requestId, metadata);
// Set start and end dates
result.StartDate = startDate;
result.EndDate = endDate;
// Ensure RequestId is set - required for PostgreSQL NOT NULL constraint
if (string.IsNullOrEmpty(result.RequestId))
{
result.RequestId = Guid.NewGuid().ToString();
}
if (save && user != null)
{
_backtestRepository.InsertBacktestForUser(user, result);
}
return result;
return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata);
}
catch (Exception ex)
{
@@ -172,8 +153,10 @@ namespace Managing.Application.Backtesting
/// <param name="candles">The candles to use for backtesting</param>
/// <param name="user">The user running the backtest (optional)</param>
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <returns>The backtest results</returns>
public async Task<Backtest> RunTradingBotBacktest(
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The lightweight backtest results</returns>
public async Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config,
List<Candle> candles,
User user = null,
@@ -181,43 +164,49 @@ namespace Managing.Application.Backtesting
string requestId = null,
object metadata = null)
{
return await RunBacktestWithCandles(config, candles, user, withCandles, requestId, metadata);
return await RunBacktestWithCandles(config, candles, user, false, withCandles, requestId, metadata);
}
/// <summary>
/// Core backtesting logic - handles the actual backtest execution with pre-loaded candles
/// </summary>
private async Task<Backtest> RunBacktestWithCandles(
private async Task<LightBacktest> RunBacktestWithCandles(
TradingBotConfig config,
List<Candle> candles,
User user = null,
bool save = false,
bool withCandles = false,
string requestId = null,
object metadata = null)
{
var tradingBot = await _botFactory.CreateBacktestTradingBot(config);
// Ensure this is a backtest configuration
if (!config.IsForBacktest)
{
throw new InvalidOperationException("Backtest configuration must have IsForBacktest set to true");
}
// Scenario and indicators should already be loaded in constructor by BotService
// This is just a validation check to ensure everything loaded properly
if (tradingBot is TradingBot bot && !bot.Indicators.Any())
// Validate that scenario and indicators are properly loaded
if (config.Scenario == null && string.IsNullOrEmpty(config.ScenarioName))
{
throw new InvalidOperationException(
$"No indicators were loaded for scenario '{config.ScenarioName ?? config.Scenario?.Name}'. " +
"This indicates a problem with scenario loading.");
"Backtest configuration must include either Scenario object or ScenarioName");
}
tradingBot.User = user;
tradingBot.Account = await GetAccountFromConfig(config);
var result =
await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata);
if (user != null)
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
result.User = user;
var fullScenario = await _scenarioService.GetScenarioByNameAndUserAsync(config.ScenarioName, user);
config.Scenario = LightScenario.FromScenario(fullScenario);
}
return result;
// Create a clean copy of the config to avoid Orleans serialization issues
var cleanConfig = CreateCleanConfigForOrleans(config);
// Create Orleans grain for backtesting
var backtestGrain = _grainFactory.GetGrain<IBacktestTradingBotGrain>(Guid.NewGuid());
// Run the backtest using the Orleans grain and return LightBacktest directly
return await backtestGrain.RunBacktestAsync(cleanConfig, candles, user, save, withCandles, requestId,
metadata);
}
private async Task<Account> GetAccountFromConfig(TradingBotConfig config)
@@ -237,128 +226,16 @@ namespace Managing.Application.Backtesting
return candles;
}
private async Task<Backtest> GetBacktestingResult(
TradingBotConfig config,
ITradingBot bot,
List<Candle> candles,
User user = null,
bool withCandles = false,
string requestId = null,
object metadata = null)
/// <summary>
/// Creates a clean copy of the trading bot config for Orleans serialization
/// Uses LightScenario and LightIndicator to avoid FixedSizeQueue serialization issues
/// </summary>
private TradingBotConfig CreateCleanConfigForOrleans(TradingBotConfig originalConfig)
{
if (candles == null || candles.Count == 0)
{
throw new Exception("No candle to backtest");
}
var totalCandles = candles.Count;
var currentCandle = 0;
var lastLoggedPercentage = 0;
_logger.LogInformation("Starting backtest with {TotalCandles} candles for {Ticker} on {Timeframe}",
totalCandles, config.Ticker, config.Timeframe);
bot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
foreach (var candle in candles)
{
bot.OptimizedCandles.Enqueue(candle);
bot.Candles.Add(candle);
await bot.Run();
currentCandle++;
// Check if wallet balance fell below 10 USDC and break if so
var currentWalletBalance = bot.WalletBalances.Values.LastOrDefault();
if (currentWalletBalance < 10m)
{
_logger.LogWarning(
"Backtest stopped early: Wallet balance fell below 10 USDC (Current: {CurrentBalance:F2} USDC) at candle {CurrentCandle}/{TotalCandles} from {CandleDate}",
currentWalletBalance, currentCandle, totalCandles, candle.Date.ToString("yyyy-MM-dd HH:mm"));
break;
}
// Log progress every 10% or every 1000 candles, whichever comes first
var currentPercentage = (int)((double)currentCandle / totalCandles * 100);
var shouldLog = currentPercentage >= lastLoggedPercentage + 10 ||
currentCandle % 1000 == 0 ||
currentCandle == totalCandles;
if (shouldLog && currentPercentage > lastLoggedPercentage)
{
_logger.LogInformation(
"Backtest progress: {CurrentCandle}/{TotalCandles} ({Percentage}%) - Processing candle from {CandleDate}",
currentCandle, totalCandles, currentPercentage, candle.Date.ToString("yyyy-MM-dd HH:mm"));
lastLoggedPercentage = currentPercentage;
}
}
_logger.LogInformation("Backtest processing completed. Calculating final results...");
bot.Candles = new HashSet<Candle>(candles);
// Only calculate indicators values if withCandles is true
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues = null;
if (withCandles)
{
indicatorsValues = GetIndicatorsValues(bot.Config.Scenario.Indicators, candles);
}
var finalPnl = bot.GetProfitAndLoss();
var winRate = bot.GetWinRate();
var stats = TradingHelpers.GetStatistics(bot.WalletBalances);
var growthPercentage =
TradingHelpers.GetGrowthFromInitalBalance(bot.WalletBalances.FirstOrDefault().Value, finalPnl);
var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last());
var fees = bot.GetTotalFees();
var scoringParams = new BacktestScoringParams(
sharpeRatio: (double)stats.SharpeRatio,
growthPercentage: (double)growthPercentage,
hodlPercentage: (double)hodlPercentage,
winRate: winRate,
totalPnL: (double)finalPnl,
fees: (double)fees,
tradeCount: bot.Positions.Count,
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
maxDrawdown: stats.MaxDrawdown,
initialBalance: bot.WalletBalances.FirstOrDefault().Value,
tradingBalance: config.BotTradingBalance,
startDate: candles[0].Date,
endDate: candles.Last().Date,
timeframe: config.Timeframe,
moneyManagement: config.MoneyManagement
);
var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams);
// Create backtest result with conditional candles and indicators values
var result = new Backtest(config, bot.Positions, bot.Signals.ToList(),
withCandles ? candles : new List<Candle>())
{
FinalPnl = finalPnl,
WinRate = winRate,
GrowthPercentage = growthPercentage,
HodlPercentage = hodlPercentage,
Fees = fees,
WalletBalances = bot.WalletBalances.ToList(),
Statistics = stats,
IndicatorsValues = withCandles
? AggregateValues(indicatorsValues, bot.IndicatorsValues)
: new Dictionary<IndicatorType, IndicatorsResultBase>(),
Score = scoringResult.Score,
ScoreMessage = scoringResult.GenerateSummaryMessage(),
Id = Guid.NewGuid().ToString(),
RequestId = requestId,
Metadata = metadata,
StartDate = candles.FirstOrDefault()!.OpenTime,
EndDate = candles.LastOrDefault()!.OpenTime,
};
// Send notification if backtest meets criteria
await SendBacktestNotificationIfCriteriaMet(result);
return result;
// Since we're now using LightScenario in TradingBotConfig, we can just return the original config
// The conversion to LightScenario is already done when loading the scenario
return originalConfig;
}
private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest)
@@ -376,56 +253,6 @@ namespace Managing.Application.Backtesting
}
}
private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues(
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues,
Dictionary<IndicatorType, IndicatorsResultBase> botStrategiesValues)
{
// Foreach strategy type, only retrieve the values where the strategy is not present already in the bot
// Then, add the values to the bot values
var result = new Dictionary<IndicatorType, IndicatorsResultBase>();
foreach (var indicator in indicatorsValues)
{
// if (!botStrategiesValues.ContainsKey(strategy.Key))
// {
// result[strategy.Key] = strategy.Value;
// }else
// {
// result[strategy.Key] = botStrategiesValues[strategy.Key];
// }
result[indicator.Key] = indicator.Value;
}
return result;
}
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
List<Candle> candles)
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var fixedCandles = new FixedSizeQueue<Candle>(10000);
foreach (var candle in candles)
{
fixedCandles.Enqueue(candle);
}
foreach (var indicator in indicators)
{
try
{
var s = ScenarioHelpers.BuildIndicator(indicator, 10000);
s.Candles = fixedCandles;
indicatorsValues[indicator.Type] = s.GetIndicatorValues();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
return indicatorsValues;
}
public async Task<bool> DeleteBacktestAsync(string id)
{

View File

@@ -12,14 +12,14 @@ namespace Managing.Application.Bots.Base
private readonly IExchangeService _exchangeService;
private readonly IMessengerService _messengerService;
private readonly IAccountService _accountService;
private readonly ILogger<TradingBot> _tradingBotLogger;
private readonly ILogger<TradingBotBase> _tradingBotLogger;
private readonly ITradingService _tradingService;
private readonly IBotService _botService;
private readonly IBackupBotService _backupBotService;
public BotFactory(
IExchangeService exchangeService,
ILogger<TradingBot> tradingBotLogger,
ILogger<TradingBotBase> tradingBotLogger,
IMessengerService messengerService,
IAccountService accountService,
ITradingService tradingService,

View File

@@ -0,0 +1,403 @@
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Models;
using Managing.Application.Abstractions.Repositories;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Orleans.Concurrency;
using static Managing.Common.Enums;
namespace Managing.Application.Bots.Grains;
/// <summary>
/// Orleans grain for backtest trading bot operations.
/// Uses composition with TradingBotBase to maintain separation of concerns.
/// This grain is stateless and follows the exact pattern of GetBacktestingResult from Backtester.cs.
/// </summary>
[StatelessWorker]
public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
{
private readonly ILogger<BacktestTradingBotGrain> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IBacktestRepository _backtestRepository;
private bool _isDisposed = false;
public BacktestTradingBotGrain(
ILogger<BacktestTradingBotGrain> logger,
IServiceScopeFactory scopeFactory,
IBacktestRepository backtestRepository)
{
_logger = logger;
_scopeFactory = scopeFactory;
_backtestRepository = backtestRepository;
}
/// <summary>
/// Runs a complete backtest following the exact pattern of GetBacktestingResult from Backtester.cs
/// </summary>
/// <param name="config">The trading bot configuration for this backtest</param>
/// <param name="candles">The candles to use for backtesting</param>
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest</param>
/// <param name="metadata">Additional metadata to associate with this backtest</param>
/// <returns>The complete backtest result</returns>
public async Task<LightBacktest> RunBacktestAsync(
TradingBotConfig config,
List<Candle> candles,
User user = null,
bool save = false,
bool withCandles = false,
string requestId = null,
object metadata = null)
{
if (candles == null || candles.Count == 0)
{
throw new Exception("No candle to backtest");
}
// Create a fresh TradingBotBase instance for this backtest
var tradingBot = await CreateTradingBotInstance(config);
tradingBot.Start();
var totalCandles = candles.Count;
var currentCandle = 0;
var lastLoggedPercentage = 0;
_logger.LogInformation("Starting backtest with {TotalCandles} candles for {Ticker} on {Timeframe}",
totalCandles, config.Ticker, config.Timeframe);
// Initialize wallet balance with first candle
tradingBot.WalletBalances.Clear();
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
// Process all candles following the exact pattern from GetBacktestingResult
foreach (var candle in candles)
{
tradingBot.OptimizedCandles.Enqueue(candle);
tradingBot.Candles.Add(candle);
await tradingBot.Run();
currentCandle++;
// Check if wallet balance fell below 10 USDC and break if so
var currentWalletBalance = tradingBot.WalletBalances.Values.LastOrDefault();
if (currentWalletBalance < 10m)
{
_logger.LogWarning(
"Backtest stopped early: Wallet balance fell below 10 USDC (Current: {CurrentBalance:F2} USDC) at candle {CurrentCandle}/{TotalCandles} from {CandleDate}",
currentWalletBalance, currentCandle, totalCandles, candle.Date.ToString("yyyy-MM-dd HH:mm"));
break;
}
// Log progress every 10% or every 1000 candles, whichever comes first
var currentPercentage = (int)((double)currentCandle / totalCandles * 100);
var shouldLog = currentPercentage >= lastLoggedPercentage + 10 ||
currentCandle % 1000 == 0 ||
currentCandle == totalCandles;
if (shouldLog && currentPercentage > lastLoggedPercentage)
{
_logger.LogInformation(
"Backtest progress: {CurrentCandle}/{TotalCandles} ({Percentage}%) - Processing candle from {CandleDate}",
currentCandle, totalCandles, currentPercentage, candle.Date.ToString("yyyy-MM-dd HH:mm"));
lastLoggedPercentage = currentPercentage;
}
}
_logger.LogInformation("Backtest processing completed. Calculating final results...");
// Set all candles for final calculations
tradingBot.Candles = new HashSet<Candle>(candles);
// Only calculate indicators values if withCandles is true
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues = null;
if (withCandles)
{
// Convert LightScenario back to full Scenario for indicator calculations
var fullScenario = config.Scenario.ToScenario();
indicatorsValues = GetIndicatorsValues(fullScenario.Indicators, candles);
}
// Calculate final results following the exact pattern from GetBacktestingResult
var finalPnl = tradingBot.GetProfitAndLoss();
var winRate = tradingBot.GetWinRate();
var stats = TradingHelpers.GetStatistics(tradingBot.WalletBalances);
var growthPercentage =
TradingHelpers.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, finalPnl);
var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last());
var fees = tradingBot.GetTotalFees();
var scoringParams = new BacktestScoringParams(
sharpeRatio: (double)stats.SharpeRatio,
growthPercentage: (double)growthPercentage,
hodlPercentage: (double)hodlPercentage,
winRate: winRate,
totalPnL: (double)finalPnl,
fees: (double)fees,
tradeCount: tradingBot.Positions.Count,
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
maxDrawdown: stats.MaxDrawdown,
initialBalance: tradingBot.WalletBalances.FirstOrDefault().Value,
tradingBalance: config.BotTradingBalance,
startDate: candles[0].Date,
endDate: candles.Last().Date,
timeframe: config.Timeframe,
moneyManagement: config.MoneyManagement
);
var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams);
// Generate requestId if not provided
var finalRequestId = requestId ?? Guid.NewGuid().ToString();
// Create backtest result with conditional candles and indicators values
var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals.ToList(),
withCandles ? candles : new List<Candle>())
{
FinalPnl = finalPnl,
WinRate = winRate,
GrowthPercentage = growthPercentage,
HodlPercentage = hodlPercentage,
Fees = fees,
WalletBalances = tradingBot.WalletBalances.ToList(),
Statistics = stats,
IndicatorsValues = withCandles
? AggregateValues(indicatorsValues, tradingBot.IndicatorsValues)
: new Dictionary<IndicatorType, IndicatorsResultBase>(),
Score = scoringResult.Score,
ScoreMessage = scoringResult.GenerateSummaryMessage(),
Id = Guid.NewGuid().ToString(),
RequestId = finalRequestId,
Metadata = metadata,
StartDate = candles.FirstOrDefault()!.OpenTime,
EndDate = candles.LastOrDefault()!.OpenTime,
};
if (save && user != null)
{
_backtestRepository.InsertBacktestForUser(user, result);
}
// Send notification if backtest meets criteria
await SendBacktestNotificationIfCriteriaMet(result);
// Clean up the trading bot instance
tradingBot.Stop();
// Convert Backtest to LightBacktest for safe Orleans serialization
return ConvertToLightBacktest(result);
}
/// <summary>
/// Converts a Backtest to LightBacktest for safe Orleans serialization
/// </summary>
/// <param name="backtest">The full backtest to convert</param>
/// <returns>A lightweight backtest suitable for Orleans serialization</returns>
private LightBacktest ConvertToLightBacktest(Backtest backtest)
{
return new LightBacktest
{
Id = backtest.Id,
Config = backtest.Config,
FinalPnl = backtest.FinalPnl,
WinRate = backtest.WinRate,
GrowthPercentage = backtest.GrowthPercentage,
HodlPercentage = backtest.HodlPercentage,
StartDate = backtest.StartDate,
EndDate = backtest.EndDate,
MaxDrawdown = backtest.Statistics?.MaxDrawdown,
Fees = backtest.Fees,
SharpeRatio = (double?)backtest.Statistics?.SharpeRatio,
Score = backtest.Score,
ScoreMessage = backtest.ScoreMessage
};
}
/// <summary>
/// Creates a TradingBotBase instance using composition for backtesting
/// </summary>
private async Task<TradingBotBase> CreateTradingBotInstance(TradingBotConfig config, User user = null)
{
// Validate configuration for backtesting
if (config == null)
{
throw new InvalidOperationException("Bot configuration is not initialized");
}
if (!config.IsForBacktest)
{
throw new InvalidOperationException("BacktestTradingBotGrain can only be used for backtesting");
}
// Create the trading bot instance
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
// Set the user if available
if (user != null)
{
tradingBot.User = user;
}
return tradingBot;
}
/// <summary>
/// Sends notification if backtest meets criteria (following Backtester.cs pattern)
/// </summary>
private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest)
{
try
{
if (backtest.Score > 60)
{
// Note: In a real implementation, you would inject IMessengerService
// For now, we'll just log the notification
_logger.LogInformation("Backtest {BacktestId} scored {Score} - notification criteria met",
backtest.Id, backtest.Score);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send backtest notification for backtest {Id}", backtest.Id);
}
}
/// <summary>
/// Aggregates indicator values (following Backtester.cs pattern)
/// </summary>
private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues(
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues,
Dictionary<IndicatorType, IndicatorsResultBase> botStrategiesValues)
{
var result = new Dictionary<IndicatorType, IndicatorsResultBase>();
foreach (var indicator in indicatorsValues)
{
result[indicator.Key] = indicator.Value;
}
return result;
}
/// <summary>
/// Gets indicators values (following Backtester.cs pattern)
/// </summary>
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
List<Candle> candles)
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var fixedCandles = new FixedSizeQueue<Candle>(10000);
foreach (var candle in candles)
{
fixedCandles.Enqueue(candle);
}
foreach (var indicator in indicators)
{
try
{
var s = ScenarioHelpers.BuildIndicator(indicator, 10000);
s.Candles = fixedCandles;
indicatorsValues[indicator.Type] = s.GetIndicatorValues();
}
catch (Exception e)
{
_logger.LogError(e, "Error building indicator {IndicatorType}", indicator.Type);
}
}
return indicatorsValues;
}
public void Dispose()
{
if (!_isDisposed)
{
_isDisposed = true;
}
}
public Task<BacktestProgress> GetBacktestProgressAsync()
{
throw new NotImplementedException();
}
public Task StartAsync()
{
throw new NotImplementedException();
}
public Task StopAsync()
{
throw new NotImplementedException();
}
public Task<BotStatus> GetStatusAsync()
{
throw new NotImplementedException();
}
public Task<TradingBotConfig> GetConfigurationAsync()
{
throw new NotImplementedException();
}
public Task<Position> OpenPositionManuallyAsync(TradeDirection direction)
{
throw new NotImplementedException();
}
public Task ToggleIsForWatchOnlyAsync()
{
throw new NotImplementedException();
}
public Task<TradingBotResponse> GetBotDataAsync()
{
throw new NotImplementedException();
}
public Task LoadBackupAsync(BotBackup backup)
{
throw new NotImplementedException();
}
public Task SaveBackupAsync()
{
throw new NotImplementedException();
}
public Task<decimal> GetProfitAndLossAsync()
{
throw new NotImplementedException();
}
public Task<int> GetWinRateAsync()
{
throw new NotImplementedException();
}
public Task<long> GetExecutionCountAsync()
{
throw new NotImplementedException();
}
public Task<DateTime> GetStartupTimeAsync()
{
throw new NotImplementedException();
}
public Task<DateTime> GetCreateDateAsync()
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,490 @@
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Models;
using Managing.Domain.Bots;
using Managing.Domain.Trades;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Bots.Grains;
/// <summary>
/// Orleans grain for live trading bot operations.
/// Uses composition with TradingBotBase to maintain separation of concerns.
/// This grain handles live trading scenarios with real-time market data and execution.
/// </summary>
public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
{
private readonly ILogger<LiveTradingBotGrain> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private TradingBotBase? _tradingBot;
private IDisposable? _timer;
private bool _isDisposed = false;
public LiveTradingBotGrain(
ILogger<LiveTradingBotGrain> logger,
IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
public override async Task OnActivateAsync(CancellationToken cancellationToken)
{
await base.OnActivateAsync(cancellationToken);
_logger.LogInformation("LiveTradingBotGrain {GrainId} activated", this.GetPrimaryKey());
// Initialize the grain state if not already done
if (!State.IsInitialized)
{
State.Identifier = this.GetPrimaryKey().ToString();
State.CreateDate = DateTime.UtcNow;
State.Status = BotStatus.Down;
State.IsInitialized = true;
await WriteStateAsync();
}
}
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
{
_logger.LogInformation("LiveTradingBotGrain {GrainId} deactivating. Reason: {Reason}",
this.GetPrimaryKey(), reason.Description);
// Stop the timer and trading bot
await StopAsync();
await base.OnDeactivateAsync(reason, cancellationToken);
}
public async Task StartAsync()
{
try
{
if (State.Status == BotStatus.Up)
{
_logger.LogWarning("Bot {GrainId} is already running", this.GetPrimaryKey());
return;
}
if (State.Config == null || string.IsNullOrEmpty(State.Config.Name))
{
throw new InvalidOperationException("Bot configuration is not properly initialized");
}
// Ensure this is not a backtest configuration
if (State.Config.IsForBacktest)
{
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
}
// Create the TradingBotBase instance using composition
_tradingBot = await CreateTradingBotInstance();
// Load backup if available
if (State.User != null)
{
await LoadBackupFromState();
}
// Start the trading bot
_tradingBot.Start();
// Update state
State.Status = BotStatus.Up;
State.StartupTime = DateTime.UtcNow;
await WriteStateAsync();
// Start Orleans timer for periodic execution
StartTimer();
_logger.LogInformation("LiveTradingBotGrain {GrainId} started successfully", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
State.Status = BotStatus.Down;
await WriteStateAsync();
throw;
}
}
public async Task StopAsync()
{
try
{
// Stop the timer
_timer?.Dispose();
_timer = null;
// Stop the trading bot
if (_tradingBot != null)
{
_tradingBot.Stop();
// Save backup before stopping
await SaveBackupToState();
_tradingBot = null;
}
// Update state
State.Status = BotStatus.Down;
await WriteStateAsync();
_logger.LogInformation("LiveTradingBotGrain {GrainId} stopped successfully", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to stop LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public Task<BotStatus> GetStatusAsync()
{
return Task.FromResult(State.Status);
}
public Task<TradingBotConfig> GetConfigurationAsync()
{
return Task.FromResult(State.Config);
}
public async Task<bool> UpdateConfigurationAsync(TradingBotConfig newConfig)
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
// Ensure this is not a backtest configuration
if (newConfig.IsForBacktest)
{
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
}
// Update the configuration in the trading bot
var success = await _tradingBot.UpdateConfiguration(newConfig);
if (success)
{
// Update the state
State.Config = newConfig;
await WriteStateAsync();
}
return success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update configuration for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
return false;
}
}
public async Task<Position> OpenPositionManuallyAsync(TradeDirection direction)
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
return await _tradingBot.OpenPositionManually(direction);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to open manual position for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task ToggleIsForWatchOnlyAsync()
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
await _tradingBot.ToggleIsForWatchOnly();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to toggle watch-only mode for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task<TradingBotResponse> GetBotDataAsync()
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
return new TradingBotResponse
{
Identifier = State.Identifier,
Name = State.Name,
Status = State.Status,
Config = State.Config,
Positions = _tradingBot.Positions,
Signals = _tradingBot.Signals.ToList(),
WalletBalances = _tradingBot.WalletBalances,
ProfitAndLoss = _tradingBot.GetProfitAndLoss(),
WinRate = _tradingBot.GetWinRate(),
ExecutionCount = _tradingBot.ExecutionCount,
StartupTime = State.StartupTime,
CreateDate = State.CreateDate
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get bot data for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task LoadBackupAsync(BotBackup backup)
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
_tradingBot.LoadBackup(backup);
// Update state from backup
State.User = backup.User;
State.Identifier = backup.Identifier;
State.Status = backup.LastStatus;
State.CreateDate = backup.Data.CreateDate;
State.StartupTime = backup.Data.StartupTime;
await WriteStateAsync();
_logger.LogInformation("Backup loaded successfully for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load backup for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task SaveBackupAsync()
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
await _tradingBot.SaveBackup();
await SaveBackupToState();
_logger.LogInformation("Backup saved successfully for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save backup for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task<decimal> GetProfitAndLossAsync()
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
return _tradingBot.GetProfitAndLoss();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get P&L for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task<int> GetWinRateAsync()
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
return _tradingBot.GetWinRate();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get win rate for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public Task<long> GetExecutionCountAsync()
{
return Task.FromResult(State.ExecutionCount);
}
public Task<DateTime> GetStartupTimeAsync()
{
return Task.FromResult(State.StartupTime);
}
public Task<DateTime> GetCreateDateAsync()
{
return Task.FromResult(State.CreateDate);
}
/// <summary>
/// Creates a TradingBotBase instance using composition
/// </summary>
private async Task<TradingBotBase> CreateTradingBotInstance()
{
// Validate configuration for live trading
if (State.Config == null)
{
throw new InvalidOperationException("Bot configuration is not initialized");
}
if (State.Config.IsForBacktest)
{
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
}
if (string.IsNullOrEmpty(State.Config.AccountName))
{
throw new InvalidOperationException("Account name is required for live trading");
}
// Create the trading bot instance
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, State.Config);
// Set the user if available
if (State.User != null)
{
tradingBot.User = State.User;
}
return tradingBot;
}
/// <summary>
/// Starts the Orleans timer for periodic bot execution
/// </summary>
private void StartTimer()
{
if (_tradingBot == null) return;
var interval = _tradingBot.Interval;
_timer = RegisterTimer(
async _ => await ExecuteBotCycle(),
null,
TimeSpan.FromMilliseconds(interval),
TimeSpan.FromMilliseconds(interval));
}
/// <summary>
/// Executes one cycle of the trading bot
/// </summary>
private async Task ExecuteBotCycle()
{
try
{
if (_tradingBot == null || State.Status != BotStatus.Up)
{
return;
}
// Execute the bot's Run method
await _tradingBot.Run();
// Update execution count
State.ExecutionCount++;
await SaveBackupToState();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during bot execution cycle for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
}
}
/// <summary>
/// Saves the current bot state to Orleans state storage
/// </summary>
private async Task SaveBackupToState()
{
if (_tradingBot == null) return;
try
{
// Sync state from TradingBotBase
State.Config = _tradingBot.Config;
State.Signals = _tradingBot.Signals;
State.Positions = _tradingBot.Positions;
State.WalletBalances = _tradingBot.WalletBalances;
State.PreloadSince = _tradingBot.PreloadSince;
State.PreloadedCandlesCount = _tradingBot.PreloadedCandlesCount;
State.Interval = _tradingBot.Interval;
State.MaxSignals = _tradingBot._maxSignals;
State.LastBackupTime = DateTime.UtcNow;
await WriteStateAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
}
}
/// <summary>
/// Loads bot state from Orleans state storage
/// </summary>
private async Task LoadBackupFromState()
{
if (_tradingBot == null) return;
try
{
// Sync state to TradingBotBase
_tradingBot.Signals = State.Signals;
_tradingBot.Positions = State.Positions;
_tradingBot.WalletBalances = State.WalletBalances;
_tradingBot.PreloadSince = State.PreloadSince;
_tradingBot.PreloadedCandlesCount = State.PreloadedCandlesCount;
_tradingBot.Config = State.Config;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
}
}
public void Dispose()
{
if (!_isDisposed)
{
_timer?.Dispose();
_isDisposed = true;
}
}
}

View File

@@ -9,12 +9,12 @@ namespace Managing.Application.Bots
{
public class SimpleBot : Bot
{
public readonly ILogger<TradingBot> Logger;
public readonly ILogger<TradingBotBase> Logger;
private readonly IBotService _botService;
private readonly IBackupBotService _backupBotService;
private Workflow _workflow;
public SimpleBot(string name, ILogger<TradingBot> logger, Workflow workflow, IBotService botService,
public SimpleBot(string name, ILogger<TradingBotBase> logger, Workflow workflow, IBotService botService,
IBackupBotService backupBotService) :
base(name)
{

View File

@@ -22,9 +22,9 @@ using static Managing.Common.Enums;
namespace Managing.Application.Bots;
public class TradingBot : Bot, ITradingBot
public class TradingBotBase : Bot, ITradingBot
{
public readonly ILogger<TradingBot> Logger;
public readonly ILogger<TradingBotBase> Logger;
private readonly IServiceScopeFactory _scopeFactory;
public TradingBotConfig Config { get; set; }
@@ -41,8 +41,8 @@ public class TradingBot : Bot, ITradingBot
public int _maxSignals = 10; // Maximum number of signals to keep in memory
public TradingBot(
ILogger<TradingBot> logger,
public TradingBotBase(
ILogger<TradingBotBase> logger,
IServiceScopeFactory scopeFactory,
TradingBotConfig config
)
@@ -71,7 +71,9 @@ public class TradingBot : Bot, ITradingBot
// Load indicators if scenario is provided in config
if (Config.Scenario != null)
{
LoadIndicators(Config.Scenario);
// Convert LightScenario to full Scenario for indicator loading
var fullScenario = Config.Scenario.ToScenario();
LoadIndicators(fullScenario);
}
else
{
@@ -151,8 +153,6 @@ public class TradingBot : Bot, ITradingBot
}
});
}
}
public async Task LoadAccount()
@@ -185,8 +185,8 @@ public class TradingBot : Bot, ITradingBot
}
else
{
// Store the scenario in config and load indicators
Config.Scenario = scenario;
// Convert full Scenario to LightScenario for storage and load indicators
Config.Scenario = LightScenario.FromScenario(scenario);
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
Logger.LogInformation($"Loaded scenario '{scenario.Name}' with {Indicators.Count} indicators");
@@ -1594,6 +1594,9 @@ public class TradingBot : Bot, ITradingBot
public override async Task SaveBackup()
{
if (Config.IsForBacktest)
return;
var data = new TradingBotBackup
{
Config = Config,
@@ -1908,7 +1911,9 @@ public class TradingBot : Bot, ITradingBot
{
if (newConfig.Scenario != null)
{
LoadScenario(newConfig.Scenario);
// Convert LightScenario to full Scenario for loading
var fullScenario = newConfig.Scenario.ToScenario();
LoadScenario(fullScenario);
// Compare indicators after scenario change
var newIndicators = Indicators?.ToList() ?? new List<IIndicator>();
@@ -2068,14 +2073,14 @@ public class TradingBot : Bot, ITradingBot
}
var isInCooldown = positionClosingDate >= cooldownCandle.Date;
if (isInCooldown)
{
var intervalMilliseconds = CandleExtensions.GetIntervalFromTimeframe(Config.Timeframe);
var intervalMinutes = intervalMilliseconds / (1000.0 * 60.0); // Convert milliseconds to minutes
var cooldownEndTime = cooldownCandle.Date.AddMinutes(intervalMinutes * Config.CooldownPeriod);
var remainingTime = cooldownEndTime - DateTime.UtcNow;
Logger.LogWarning(
$"⏳ **Cooldown Period Active**\n" +
$"Cannot open new positions\n" +

View File

@@ -0,0 +1,117 @@
using Managing.Domain.Bots;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using static Managing.Common.Enums;
namespace Managing.Application.Bots;
/// <summary>
/// Orleans grain state for TradingBot.
/// This class represents the persistent state of a trading bot grain.
/// All properties must be serializable for Orleans state management.
/// </summary>
[GenerateSerializer]
public class TradingBotGrainState
{
/// <summary>
/// The trading bot configuration
/// </summary>
[Id(0)]
public TradingBotConfig Config { get; set; } = new();
/// <summary>
/// Collection of trading signals generated by the bot
/// </summary>
[Id(1)]
public HashSet<LightSignal> Signals { get; set; } = new();
/// <summary>
/// List of trading positions opened by the bot
/// </summary>
[Id(2)]
public List<Position> Positions { get; set; } = new();
/// <summary>
/// Historical wallet balances tracked over time
/// </summary>
[Id(3)]
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new();
/// <summary>
/// Current status of the bot (Running, Stopped, etc.)
/// </summary>
[Id(4)]
public BotStatus Status { get; set; } = BotStatus.Down;
/// <summary>
/// When the bot was started
/// </summary>
[Id(5)]
public DateTime StartupTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// When the bot was created
/// </summary>
[Id(6)]
public DateTime CreateDate { get; set; } = DateTime.UtcNow;
/// <summary>
/// The user who owns this bot
/// </summary>
[Id(7)]
public User User { get; set; }
/// <summary>
/// Bot execution counter
/// </summary>
[Id(8)]
public long ExecutionCount { get; set; } = 0;
/// <summary>
/// Bot identifier/name
/// </summary>
[Id(9)]
public string Identifier { get; set; } = string.Empty;
/// <summary>
/// Bot display name
/// </summary>
[Id(10)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Preload start date for candles
/// </summary>
[Id(11)]
public DateTime PreloadSince { get; set; } = DateTime.UtcNow;
/// <summary>
/// Number of preloaded candles
/// </summary>
[Id(12)]
public int PreloadedCandlesCount { get; set; } = 0;
/// <summary>
/// Timer interval for bot execution
/// </summary>
[Id(13)]
public int Interval { get; set; } = 60000; // Default 1 minute
/// <summary>
/// Maximum number of signals to keep in memory
/// </summary>
[Id(14)]
public int MaxSignals { get; set; } = 10;
/// <summary>
/// Indicates if the bot has been initialized
/// </summary>
[Id(15)]
public bool IsInitialized { get; set; } = false;
/// <summary>
/// Last time the bot state was persisted
/// </summary>
[Id(16)]
public DateTime LastBackupTime { get; set; } = DateTime.UtcNow;
}

View File

@@ -776,7 +776,7 @@ public class TradingBotChromosome : ChromosomeBase
UseForPositionSizing = false,
UseForSignalFiltering = false,
UseForDynamicStopLoss = false,
Scenario = scenario,
Scenario = LightScenario.FromScenario(scenario),
MoneyManagement = mm,
RiskManagement = new RiskManagement
{
@@ -915,7 +915,7 @@ public class TradingBotFitness : IFitness
var currentGeneration = _geneticAlgorithm?.GenerationsNumber ?? 0;
// Run backtest using scoped service to avoid DbContext concurrency issues
var backtest = ServiceScopeHelpers.WithScopedService<IBacktester, Backtest>(
var lightBacktest = ServiceScopeHelpers.WithScopedService<IBacktester, LightBacktest>(
_serviceScopeFactory,
backtester => backtester.RunTradingBotBacktest(
config,
@@ -933,7 +933,7 @@ public class TradingBotFitness : IFitness
).Result;
// Calculate multi-objective fitness based on backtest results
var fitness = CalculateFitness(backtest, config);
var fitness = CalculateFitness(lightBacktest, config);
return fitness;
}
@@ -945,13 +945,13 @@ public class TradingBotFitness : IFitness
}
}
private double CalculateFitness(Backtest backtest, TradingBotConfig config)
private double CalculateFitness(LightBacktest lightBacktest, TradingBotConfig config)
{
if (backtest == null || backtest.Statistics == null)
if (lightBacktest == null)
return 0.1;
// Calculate base fitness from backtest score
var baseFitness = backtest.Score;
var baseFitness = lightBacktest.Score;
// Return base fitness (no penalty for now)
return baseFitness;

View File

@@ -4,6 +4,7 @@ using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots;
using Managing.Domain.Bots;
using Managing.Domain.Scenarios;
using Managing.Domain.Users;
using Managing.Domain.Workflows;
using Microsoft.Extensions.DependencyInjection;
@@ -18,20 +19,21 @@ namespace Managing.Application.ManageBot
private readonly IExchangeService _exchangeService;
private readonly IMessengerService _messengerService;
private readonly IAccountService _accountService;
private readonly ILogger<TradingBot> _tradingBotLogger;
private readonly ILogger<TradingBotBase> _tradingBotLogger;
private readonly ITradingService _tradingService;
private readonly IMoneyManagementService _moneyManagementService;
private readonly IUserService _userService;
private readonly IBackupBotService _backupBotService;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IGrainFactory _grainFactory;
private ConcurrentDictionary<string, BotTaskWrapper> _botTasks =
new ConcurrentDictionary<string, BotTaskWrapper>();
public BotService(IBotRepository botRepository, IExchangeService exchangeService,
IMessengerService messengerService, IAccountService accountService, ILogger<TradingBot> tradingBotLogger,
IMessengerService messengerService, IAccountService accountService, ILogger<TradingBotBase> tradingBotLogger,
ITradingService tradingService, IMoneyManagementService moneyManagementService, IUserService userService,
IBackupBotService backupBotService, IServiceScopeFactory scopeFactory)
IBackupBotService backupBotService, IServiceScopeFactory scopeFactory, IGrainFactory grainFactory)
{
_botRepository = botRepository;
_exchangeService = exchangeService;
@@ -43,26 +45,26 @@ namespace Managing.Application.ManageBot
_userService = userService;
_backupBotService = backupBotService;
_scopeFactory = scopeFactory;
_grainFactory = grainFactory;
}
public class BotTaskWrapper
{
public Task Task { get; private set; }
public Type BotType { get; private set; }
public object BotInstance { get; private set; } // Add this line
public object BotInstance { get; private set; }
public BotTaskWrapper(Task task, Type botType, object botInstance) // Update constructor
public BotTaskWrapper(Task task, Type botType, object botInstance)
{
Task = task;
BotType = botType;
BotInstance = botInstance; // Set the bot instance
BotInstance = botInstance;
}
}
public void AddSimpleBotToCache(IBot bot)
{
var botTask =
new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot); // Pass bot as the instance
var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot);
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
}
@@ -72,24 +74,34 @@ namespace Managing.Application.ManageBot
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
}
private async Task InitBot(ITradingBot bot, BotBackup backupBot)
{
var user = await _userService.GetUser(backupBot.User.Name);
bot.User = user;
// Config is already set correctly from backup data, so we only need to restore signals, positions, etc.
bot.LoadBackup(backupBot);
try
{
var user = await _userService.GetUser(backupBot.User.Name);
bot.User = user;
// Load backup data into the bot
bot.LoadBackup(backupBot);
// Only start the bot if the backup status is Up
if (backupBot.LastStatus == BotStatus.Up)
{
// Start the bot asynchronously without waiting for completion
_ = Task.Run(() => bot.Start());
// Only start the bot if the backup status is Up
if (backupBot.LastStatus == BotStatus.Up)
{
// Start the bot asynchronously without waiting for completion
_ = Task.Run(() => bot.Start());
}
else
{
// Keep the bot in Down status if it was originally Down
bot.Stop();
}
}
else
catch (Exception ex)
{
// Keep the bot in Down status if it was originally Down
_tradingBotLogger.LogError(ex, "Error initializing bot {Identifier} from backup", backupBot.Identifier);
// Ensure the bot is stopped if initialization fails
bot.Stop();
throw;
}
}
@@ -137,7 +149,7 @@ namespace Managing.Application.ManageBot
var scenario = await _tradingService.GetScenarioByNameAsync(scalpingConfig.ScenarioName);
if (scenario != null)
{
scalpingConfig.Scenario = scenario;
scalpingConfig.Scenario = LightScenario.FromScenario(scenario);
}
else
{
@@ -155,6 +167,10 @@ namespace Managing.Application.ManageBot
// Ensure critical properties are set correctly for restored bots
scalpingConfig.IsForBacktest = false;
// IMPORTANT: Save the backup to database BEFORE creating the Orleans grain
// This ensures the backup exists when the grain tries to serialize it
await SaveOrUpdateBotBackup(backupBot.User, backupBot.Identifier, backupBot.LastStatus, backupBot.Data);
bot = await CreateTradingBot(scalpingConfig);
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
@@ -206,7 +222,7 @@ namespace Managing.Application.ManageBot
if (botWrapper.BotInstance is IBot bot)
{
await Task.Run(() =>
bot.Stop()); // Assuming Stop is an asynchronous process wrapped in Task.Run for synchronous methods
bot.Stop());
var stopMessage = $"🛑 **Bot Stopped**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" +
@@ -231,7 +247,7 @@ namespace Managing.Application.ManageBot
if (botWrapper.BotInstance is IBot bot)
{
await Task.Run(() =>
bot.Stop()); // Assuming Stop is an asynchronous process wrapped in Task.Run for synchronous methods
bot.Stop());
var deleteMessage = $"🗑️ **Bot Deleted**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" +
@@ -306,7 +322,7 @@ namespace Managing.Application.ManageBot
public async Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig)
{
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) &&
botTaskWrapper.BotInstance is TradingBot tradingBot)
botTaskWrapper.BotInstance is TradingBotBase tradingBot)
{
// Ensure the scenario is properly loaded from database if needed
if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName))
@@ -314,7 +330,7 @@ namespace Managing.Application.ManageBot
var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName);
if (scenario != null)
{
newConfig.Scenario = scenario;
newConfig.Scenario = LightScenario.FromScenario(scenario);
}
else
{
@@ -370,7 +386,6 @@ namespace Managing.Application.ManageBot
return false;
}
public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
@@ -379,7 +394,7 @@ namespace Managing.Application.ManageBot
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
config.Scenario = LightScenario.FromScenario(scenario);
}
else
{
@@ -392,7 +407,15 @@ namespace Managing.Application.ManageBot
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
return new TradingBot(_tradingBotLogger, _scopeFactory, config);
// For now, use TradingBot for both live trading and backtesting
// TODO: Implement Orleans grain for live trading when ready
if (!config.IsForBacktest)
{
// Ensure critical properties are set correctly for live trading
config.IsForBacktest = false;
}
return new TradingBotBase(_tradingBotLogger, _scopeFactory, config);
}
public async Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config)
@@ -403,7 +426,7 @@ namespace Managing.Application.ManageBot
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
config.Scenario = LightScenario.FromScenario(scenario);
}
else
{
@@ -417,109 +440,7 @@ namespace Managing.Application.ManageBot
}
config.IsForBacktest = true;
return new TradingBot(_tradingBotLogger, _scopeFactory, config);
}
public async Task<ITradingBot> CreateScalpingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
config.FlipPosition = false;
return new TradingBot(_tradingBotLogger, _scopeFactory, config);
}
public async Task<ITradingBot> CreateBacktestScalpingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
config.IsForBacktest = true;
config.FlipPosition = false;
return new TradingBot(_tradingBotLogger, _scopeFactory, config);
}
public async Task<ITradingBot> CreateFlippingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
config.FlipPosition = true;
return new TradingBot(_tradingBotLogger, _scopeFactory, config);
}
public async Task<ITradingBot> CreateBacktestFlippingBot(TradingBotConfig config)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null)
{
config.Scenario = scenario;
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
config.IsForBacktest = true;
config.FlipPosition = true;
return new TradingBot(_tradingBotLogger, _scopeFactory, config);
return new TradingBotBase(_tradingBotLogger, _scopeFactory, config);
}
}
}

View File

@@ -46,7 +46,7 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
// Try to get the active bot multiple times to ensure it's properly started
int attempts = 0;
const int maxAttempts = 5;
const int maxAttempts = 2;
while (attempts < maxAttempts)
{
@@ -58,7 +58,8 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
if (backupBot.LastStatus == BotStatus.Down)
{
result[activeBot.Identifier] = BotStatus.Down;
_logger.LogInformation("Backup bot {Identifier} loaded but kept in Down status as it was originally Down.",
_logger.LogInformation(
"Backup bot {Identifier} loaded but kept in Down status as it was originally Down.",
backupBot.Identifier);
}
else
@@ -68,6 +69,7 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
_logger.LogInformation("Backup bot {Identifier} started successfully.",
backupBot.Identifier);
}
break;
}

View File

@@ -7,29 +7,31 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="MoneyManagements\Abstractions\**"/>
<EmbeddedResource Remove="MoneyManagements\Abstractions\**"/>
<None Remove="MoneyManagements\Abstractions\**"/>
<Compile Remove="MoneyManagements\Abstractions\**" />
<EmbeddedResource Remove="MoneyManagements\Abstractions\**" />
<None Remove="MoneyManagements\Abstractions\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.1"/>
<PackageReference Include="GeneticSharp" Version="3.1.4"/>
<PackageReference Include="MediatR" Version="12.2.0"/>
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0"/>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1"/>
<PackageReference Include="Polly" Version="8.4.0"/>
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0"/>
<PackageReference Include="FluentValidation" Version="11.9.1" />
<PackageReference Include="GeneticSharp" Version="3.1.4" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="Microsoft.Orleans.Client" Version="9.2.1" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" />
<PackageReference Include="Polly" Version="8.4.0" />
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj"/>
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj"/>
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj"/>
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj" />
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj" />
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj" />
</ItemGroup>
</Project>

View File

@@ -302,5 +302,16 @@ namespace Managing.Application.Scenarios
return result;
}
public async Task<Scenario> GetScenarioByNameAndUserAsync(string scenarioName, User user)
{
var scenario = await _tradingService.GetScenarioByNameAsync(scenarioName);
if (scenario == null)
{
throw new InvalidOperationException($"Scenario {scenarioName} not found for user {user.Name}");
}
return scenario;
}
}
}

View File

@@ -42,6 +42,8 @@ using MediatR;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Managing.Bootstrap;
@@ -60,9 +62,62 @@ public static class ApiBootstrap
.AddInfrastructure(configuration)
.AddWorkers(configuration)
.AddFluentValidation()
.AddMediatR();
.AddMediatR()
;
}
// Note: IClusterClient is automatically available in co-hosting scenarios
// through IGrainFactory. Services should inject IGrainFactory instead of IClusterClient
// to avoid circular dependency issues during DI container construction.
public static IHostBuilder ConfigureOrleans(this IHostBuilder hostBuilder, IConfiguration configuration,
bool isProduction)
{
var postgreSqlConnectionString = configuration.GetSection("Databases:PostgreSql")["ConnectionString"];
return hostBuilder.UseOrleans(siloBuilder =>
{
// Configure clustering
if (isProduction && !string.IsNullOrEmpty(postgreSqlConnectionString))
{
// Production clustering configuration
siloBuilder
.UseAdoNetClustering(options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
})
.UseAdoNetReminderService(options =>
{
options.ConnectionString = postgreSqlConnectionString;
options.Invariant = "Npgsql";
});
}
else
{
// Development clustering configuration
siloBuilder.UseLocalhostClustering();
}
siloBuilder
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Information))
.UseDashboard(options => { })
.AddMemoryGrainStorageAsDefault()
.ConfigureServices(services =>
{
// Register existing services for Orleans DI
// These will be available to grains through dependency injection
services.AddTransient<IExchangeService, ExchangeService>();
services.AddTransient<IAccountService, AccountService>();
services.AddTransient<ITradingService, TradingService>();
services.AddTransient<IMessengerService, MessengerService>();
services.AddTransient<IBackupBotService, BackupBotService>();
});
})
;
}
private static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddScoped<ITradingService, TradingService>();

View File

@@ -7,22 +7,28 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.1"/>
<PackageReference Include="MediatR" Version="12.2.0"/>
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0"/>
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0"/>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.1" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />
<PackageReference Include="Microsoft.Orleans.Client" Version="9.2.1" />
<PackageReference Include="Microsoft.Orleans.Clustering.AdoNet" Version="9.2.1" />
<PackageReference Include="Microsoft.Orleans.Reminders.AdoNet" Version="9.2.1" />
<PackageReference Include="Microsoft.Orleans.Server" Version="9.2.1" />
<PackageReference Include="OrleansDashboard" Version="8.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Application.Workers\Managing.Application.Workers.csproj"/>
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Exchanges\Managing.Infrastructure.Exchanges.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Messengers\Managing.Infrastructure.Messengers.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Storage\Managing.Infrastructure.Storage.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Web3\Managing.Infrastructure.Evm.csproj"/>
<ProjectReference Include="..\Managing.Application.Workers\Managing.Application.Workers.csproj" />
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Exchanges\Managing.Infrastructure.Exchanges.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Messengers\Managing.Infrastructure.Messengers.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Storage\Managing.Infrastructure.Storage.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Web3\Managing.Infrastructure.Evm.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,17 +1,32 @@
using Managing.Domain.Users;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Managing.Domain.Users;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Domain.Accounts;
[GenerateSerializer]
public class Account
{
[Id(0)]
[Required] public string Name { get; set; }
[Id(1)]
[Required] public TradingExchanges Exchange { get; set; }
[Id(2)]
[Required] public AccountType Type { get; set; }
[Id(3)]
public string Key { get; set; }
[Id(4)]
public string Secret { get; set; }
[Id(5)]
public User User { get; set; }
[Id(6)]
public List<Balance> Balances { get; set; }
public bool IsPrivyWallet => Type == AccountType.Privy;

View File

@@ -1,14 +1,29 @@
using Managing.Domain.Evm;
using Orleans;
namespace Managing.Domain.Accounts;
[GenerateSerializer]
public class Balance
{
[Id(0)]
public string TokenImage { get; set; }
[Id(1)]
public string TokenName { get; set; }
[Id(2)]
public decimal Amount { get; set; }
[Id(3)]
public decimal Price { get; set; }
[Id(4)]
public decimal Value { get; set; }
[Id(5)]
public string TokenAdress { get; set; }
[Id(6)]
public Chain Chain { get; set; }
}

View File

@@ -1,20 +1,26 @@
using Managing.Domain.Bots;
using Orleans;
namespace Managing.Domain.Backtests;
/// <summary>
/// Lightweight backtest class for Orleans serialization
/// Contains only the essential properties needed for backtest results
/// </summary>
[GenerateSerializer]
public class LightBacktest
{
public string Id { get; set; } = string.Empty;
public TradingBotConfig Config { get; set; } = new();
public decimal FinalPnl { get; set; }
public int WinRate { get; set; }
public decimal GrowthPercentage { get; set; }
public decimal HodlPercentage { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public decimal? MaxDrawdown { get; set; }
public decimal Fees { get; set; }
public double? SharpeRatio { get; set; }
public double Score { get; set; }
public string ScoreMessage { get; set; } = string.Empty;
[Id(0)] public string Id { get; set; } = string.Empty;
[Id(1)] public TradingBotConfig Config { get; set; } = new();
[Id(2)] public decimal FinalPnl { get; set; }
[Id(3)] public int WinRate { get; set; }
[Id(4)] public decimal GrowthPercentage { get; set; }
[Id(5)] public decimal HodlPercentage { get; set; }
[Id(6)] public DateTime StartDate { get; set; }
[Id(7)] public DateTime EndDate { get; set; }
[Id(8)] public decimal? MaxDrawdown { get; set; }
[Id(9)] public decimal Fees { get; set; }
[Id(10)] public double? SharpeRatio { get; set; }
[Id(11)] public double Score { get; set; }
[Id(12)] public string ScoreMessage { get; set; } = string.Empty;
}

View File

@@ -1,15 +1,26 @@
using Managing.Domain.Users;
using Newtonsoft.Json;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Domain.Bots;
[GenerateSerializer]
public class BotBackup
{
[Id(0)]
public string Identifier { get; set; }
[Id(1)]
public User User { get; set; }
[Id(2)]
public TradingBotBackup Data { get; set; }
[Id(3)]
public BotStatus LastStatus { get; set; }
[Id(4)]
public DateTime CreateDate { get; set; }
/// <summary>

View File

@@ -1,36 +1,44 @@
using Managing.Domain.Trades;
using Orleans;
namespace Managing.Domain.Bots;
[GenerateSerializer]
public class TradingBotBackup
{
/// <summary>
/// The complete trading bot configuration
/// </summary>
[Id(0)]
public TradingBotConfig Config { get; set; }
/// <summary>
/// Runtime state: Active signals for the bot
/// </summary>
[Id(1)]
public HashSet<LightSignal> Signals { get; set; }
/// <summary>
/// Runtime state: Open and closed positions for the bot
/// </summary>
[Id(2)]
public List<Position> Positions { get; set; }
/// <summary>
/// Runtime state: Historical wallet balances over time
/// </summary>
[Id(3)]
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
/// <summary>
/// Runtime state: When the bot was started
/// </summary>
[Id(4)]
public DateTime StartupTime { get; set; }
/// <summary>
/// Runtime state: When the bot was created
/// </summary>
[Id(5)]
public DateTime CreateDate { get; set; }
}

View File

@@ -1,22 +1,45 @@
using System.ComponentModel.DataAnnotations;
using Managing.Domain.Risk;
using Managing.Domain.Scenarios;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Domain.Bots;
[GenerateSerializer]
public class TradingBotConfig
{
[Id(0)]
[Required] public string AccountName { get; set; }
[Id(1)]
[Required] public LightMoneyManagement MoneyManagement { get; set; }
[Id(2)]
[Required] public Ticker Ticker { get; set; }
[Id(3)]
[Required] public Timeframe Timeframe { get; set; }
[Id(4)]
[Required] public bool IsForWatchingOnly { get; set; }
[Id(5)]
[Required] public decimal BotTradingBalance { get; set; }
[Id(6)]
[Required] public bool IsForBacktest { get; set; }
[Id(7)]
[Required] public int CooldownPeriod { get; set; }
[Id(8)]
[Required] public int MaxLossStreak { get; set; }
[Id(9)]
[Required] public bool FlipPosition { get; set; }
[Id(10)]
[Required] public string Name { get; set; }
/// <summary>
@@ -24,23 +47,28 @@ public class TradingBotConfig
/// Contains all configurable parameters for Expected Utility Theory, Kelly Criterion, and probability thresholds.
/// If null, default risk management settings will be used.
/// </summary>
[Id(11)]
public RiskManagement RiskManagement { get; set; } = new();
/// <summary>
/// The scenario object containing all strategies. When provided, this takes precedence over ScenarioName.
/// The lightweight scenario object containing all strategies. When provided, this takes precedence over ScenarioName.
/// This allows running backtests without requiring scenarios to be saved in the database.
/// Orleans-friendly version without FixedSizeQueue and User properties.
/// </summary>
public Scenario Scenario { get; set; }
[Id(12)]
public LightScenario Scenario { get; set; }
/// <summary>
/// The scenario name to load from database. Only used when Scenario object is not provided.
/// </summary>
[Id(13)]
public string ScenarioName { get; set; }
/// <summary>
/// Maximum time in hours that a position can remain open before being automatically closed.
/// If null, time-based position closure is disabled.
/// </summary>
[Id(14)]
public decimal? MaxPositionTimeHours { get; set; }
/// <summary>
@@ -49,6 +77,7 @@ public class TradingBotConfig
/// If false, the position will only be closed when MaxPositionTimeHours is reached.
/// Default is false to maintain existing behavior.
/// </summary>
[Id(15)]
public bool CloseEarlyWhenProfitable { get; set; } = false;
/// <summary>
@@ -56,6 +85,7 @@ public class TradingBotConfig
/// If false, positions will be flipped regardless of profit status.
/// Default is true for safer trading.
/// </summary>
[Id(16)]
[Required]
public bool FlipOnlyWhenInProfit { get; set; } = true;
@@ -65,20 +95,24 @@ public class TradingBotConfig
/// When false, the bot operates in traditional mode without Synth predictions.
/// The actual Synth configuration is managed centrally in SynthPredictionService.
/// </summary>
[Id(17)]
public bool UseSynthApi { get; set; } = false;
/// <summary>
/// Whether to use Synth predictions for position sizing adjustments and risk assessment
/// </summary>
[Id(18)]
public bool UseForPositionSizing { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for signal filtering
/// </summary>
[Id(19)]
public bool UseForSignalFiltering { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary>
[Id(20)]
public bool UseForDynamicStopLoss { get; set; } = true;
}

View File

@@ -1,20 +1,41 @@
using System.ComponentModel.DataAnnotations;
using Managing.Common;
using Orleans;
using Skender.Stock.Indicators;
namespace Managing.Domain.Candles
{
[GenerateSerializer]
public class Candle : IQuote
{
[Id(0)]
[Required] public Enums.TradingExchanges Exchange { get; set; }
[Id(1)]
[Required] public string Ticker { get; set; }
[Id(2)]
[Required] public DateTime OpenTime { get; set; }
[Id(3)]
[Required] public DateTime Date { get; set; }
[Id(4)]
[Required] public decimal Open { get; set; }
[Id(5)]
[Required] public decimal Close { get; set; }
[Id(6)]
[Required] public decimal High { get; set; }
[Id(7)]
[Required] public decimal Low { get; set; }
[Id(8)]
[Required] public Enums.Timeframe Timeframe { get; set; }
[Id(9)]
public decimal Volume { get; set; }
}
}

View File

@@ -1,9 +1,19 @@
namespace Managing.Domain.Evm;
using Orleans;
namespace Managing.Domain.Evm;
[GenerateSerializer]
public class Chain
{
[Id(0)]
public string Id { get; set; }
[Id(1)]
public string RpcUrl { get; set; }
[Id(2)]
public string Name { get; set; }
[Id(3)]
public int ChainId { get; set; }
}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Exilion.TradingAtomics" Version="1.0.4"/>
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0"/>
</ItemGroup>

View File

@@ -1,17 +1,28 @@
using System.ComponentModel.DataAnnotations;
using Orleans;
using static Managing.Common.Enums;
[GenerateSerializer]
public class LightMoneyManagement
{
[Id(0)]
[Required] public string Name { get; set; }
[Id(1)]
[Required] public Timeframe Timeframe { get; set; }
[Id(2)]
[Required] public decimal StopLoss { get; set; }
[Id(3)]
[Required] public decimal TakeProfit { get; set; }
[Id(4)]
[Required] public decimal Leverage { get; set; }
public void FormatPercentage()
{
StopLoss /= 100;
TakeProfit /= 100;
}
public void FormatPercentage()
{
StopLoss /= 100;
TakeProfit /= 100;
}
}

View File

@@ -1,9 +1,12 @@
using Managing.Domain.Users;
using Orleans;
namespace Managing.Domain.MoneyManagements
{
[GenerateSerializer]
public class MoneyManagement : LightMoneyManagement
{
[Id(5)]
public User User { get; set; }
}
}

View File

@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using Managing.Common;
using Orleans;
namespace Managing.Domain.Risk;
@@ -7,6 +8,7 @@ namespace Managing.Domain.Risk;
/// Risk management configuration for trading bots
/// Contains all configurable risk parameters for probabilistic analysis and position sizing
/// </summary>
[GenerateSerializer]
public class RiskManagement
{
/// <summary>
@@ -14,6 +16,7 @@ public class RiskManagement
/// Signals with SL probability above this threshold may be filtered out
/// Range: 0.05 (5%) to 0.50 (50%)
/// </summary>
[Id(0)]
[Range(0.05, 0.50)]
[Required]
public decimal AdverseProbabilityThreshold { get; set; } = 0.20m;
@@ -23,6 +26,7 @@ public class RiskManagement
/// Used for additional signal filtering and confidence assessment
/// Range: 0.10 (10%) to 0.70 (70%)
/// </summary>
[Id(1)]
[Range(0.10, 0.70)]
[Required]
public decimal FavorableProbabilityThreshold { get; set; } = 0.30m;
@@ -32,6 +36,7 @@ public class RiskManagement
/// Higher values = more risk-averse behavior in utility calculations
/// Range: 0.1 (risk-seeking) to 5.0 (highly risk-averse)
/// </summary>
[Id(2)]
[Range(0.1, 5.0)]
[Required]
public decimal RiskAversion { get; set; } = 1.0m;
@@ -41,6 +46,7 @@ public class RiskManagement
/// Trades with Kelly fraction below this threshold are considered unfavorable
/// Range: 0.5% to 10%
/// </summary>
[Id(3)]
[Range(0.005, 0.10)]
[Required]
public decimal KellyMinimumThreshold { get; set; } = 0.01m;
@@ -50,6 +56,7 @@ public class RiskManagement
/// Prevents over-allocation even when Kelly suggests higher percentages
/// Range: 5% to 50%
/// </summary>
[Id(4)]
[Range(0.05, 0.50)]
[Required]
public decimal KellyMaximumCap { get; set; } = 0.25m;
@@ -59,6 +66,7 @@ public class RiskManagement
/// Positions with higher liquidation risk may be blocked or reduced
/// Range: 5% to 30%
/// </summary>
[Id(5)]
[Range(0.05, 0.30)]
[Required]
public decimal MaxLiquidationProbability { get; set; } = 0.10m;
@@ -68,6 +76,7 @@ public class RiskManagement
/// Longer horizons provide more stable predictions but less responsive signals
/// Range: 1 hour to 168 hours (1 week)
/// </summary>
[Id(6)]
[Range(1, 168)]
[Required]
public int SignalValidationTimeHorizonHours { get; set; } = 24;
@@ -77,6 +86,7 @@ public class RiskManagement
/// Shorter horizons for more frequent risk updates on open positions
/// Range: 1 hour to 48 hours
/// </summary>
[Id(7)]
[Range(1, 48)]
[Required]
public int PositionMonitoringTimeHorizonHours { get; set; } = 6;
@@ -86,6 +96,7 @@ public class RiskManagement
/// Positions exceeding this liquidation risk will trigger warnings
/// Range: 10% to 40%
/// </summary>
[Id(8)]
[Range(0.10, 0.40)]
[Required]
public decimal PositionWarningThreshold { get; set; } = 0.20m;
@@ -95,6 +106,7 @@ public class RiskManagement
/// Positions exceeding this liquidation risk will be automatically closed
/// Range: 30% to 80%
/// </summary>
[Id(9)]
[Range(0.30, 0.80)]
[Required]
public decimal PositionAutoCloseThreshold { get; set; } = 0.50m;
@@ -104,6 +116,7 @@ public class RiskManagement
/// Values less than 1.0 implement fractional Kelly (e.g., 0.5 = half-Kelly)
/// Range: 0.1 to 1.0
/// </summary>
[Id(10)]
[Range(0.1, 1.0)]
[Required]
public decimal KellyFractionalMultiplier { get; set; } = 1.0m;
@@ -111,18 +124,21 @@ public class RiskManagement
/// <summary>
/// Risk tolerance level affecting overall risk calculations
/// </summary>
[Id(11)]
[Required]
public Enums.RiskToleranceLevel RiskTolerance { get; set; } = Enums.RiskToleranceLevel.Moderate;
/// <summary>
/// Whether to use Expected Utility Theory for decision making
/// </summary>
[Id(12)]
[Required]
public bool UseExpectedUtility { get; set; } = true;
/// <summary>
/// Whether to use Kelly Criterion for position sizing recommendations
/// </summary>
[Id(13)]
[Required]
public bool UseKellyCriterion { get; set; } = true;

View File

@@ -0,0 +1,57 @@
using Managing.Domain.Strategies;
using Orleans;
namespace Managing.Domain.Scenarios;
/// <summary>
/// Lightweight scenario class for Orleans serialization
/// Contains only the essential properties needed for backtesting
/// </summary>
[GenerateSerializer]
public class LightScenario
{
public LightScenario(string name, int? loopbackPeriod = 1)
{
Name = name;
Indicators = new List<LightIndicator>();
LoopbackPeriod = loopbackPeriod;
}
[Id(0)] public string Name { get; set; }
[Id(1)] public List<LightIndicator> Indicators { get; set; }
[Id(2)] public int? LoopbackPeriod { get; set; }
/// <summary>
/// Converts a full Scenario to a LightScenario
/// </summary>
public static LightScenario FromScenario(Scenario scenario)
{
var lightScenario = new LightScenario(scenario.Name, scenario.LoopbackPeriod)
{
Indicators = scenario.Indicators?.Select(LightIndicator.FromIndicator).ToList() ??
new List<LightIndicator>()
};
return lightScenario;
}
/// <summary>
/// Converts a LightScenario back to a full Scenario
/// </summary>
public Scenario ToScenario()
{
var scenario = new Scenario(Name, LoopbackPeriod)
{
Indicators = Indicators?.Select(li => li.ToIndicator()).ToList() ?? new List<Indicator>()
};
return scenario;
}
public void AddIndicator(LightIndicator indicator)
{
if (Indicators == null)
Indicators = new List<LightIndicator>();
Indicators.Add(indicator);
}
}

View File

@@ -1,8 +1,10 @@
using Managing.Domain.Strategies;
using Managing.Domain.Users;
using Orleans;
namespace Managing.Domain.Scenarios
{
[GenerateSerializer]
public class Scenario
{
public Scenario(string name, int? loopbackPeriod = 1)
@@ -12,9 +14,16 @@ namespace Managing.Domain.Scenarios
LoopbackPeriod = loopbackPeriod;
}
[Id(0)]
public string Name { get; set; }
[Id(1)]
public List<Indicator> Indicators { get; set; }
[Id(2)]
public int? LoopbackPeriod { get; set; }
[Id(3)]
public User User { get; set; }
public void AddIndicator(Indicator indicator)

View File

@@ -1,6 +1,4 @@
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using Managing.Core.FixedSizedQueue;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Candles;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies.Base;
@@ -20,18 +18,31 @@ namespace Managing.Domain.Strategies
}
public string Name { get; set; }
[JsonIgnore] [IgnoreDataMember] public FixedSizeQueue<Candle> Candles { get; set; }
public FixedSizeQueue<Candle> Candles { get; set; }
public IndicatorType Type { get; set; }
public SignalType SignalType { get; set; }
public int MinimumHistory { get; set; }
public int? Period { get; set; }
public int? FastPeriods { get; set; }
public int? SlowPeriods { get; set; }
public int? SignalPeriods { get; set; }
public double? Multiplier { get; set; }
public int? SmoothPeriods { get; set; }
public int? StochPeriods { get; set; }
public int? CyclePeriods { get; set; }
public User User { get; set; }
public virtual List<LightSignal> Run()

View File

@@ -0,0 +1,84 @@
using Managing.Domain.Scenarios;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies;
/// <summary>
/// Lightweight indicator class for Orleans serialization
/// Contains only the essential properties needed for backtesting
/// </summary>
[GenerateSerializer]
public class LightIndicator
{
public LightIndicator(string name, IndicatorType type)
{
Name = name;
Type = type;
SignalType = ScenarioHelpers.GetSignalType(type);
}
[Id(0)] public string Name { get; set; }
[Id(1)] public IndicatorType Type { get; set; }
[Id(2)] public SignalType SignalType { get; set; }
[Id(3)] public int MinimumHistory { get; set; }
[Id(4)] public int? Period { get; set; }
[Id(5)] public int? FastPeriods { get; set; }
[Id(6)] public int? SlowPeriods { get; set; }
[Id(7)] public int? SignalPeriods { get; set; }
[Id(8)] public double? Multiplier { get; set; }
[Id(9)] public int? SmoothPeriods { get; set; }
[Id(10)] public int? StochPeriods { get; set; }
[Id(11)] public int? CyclePeriods { get; set; }
/// <summary>
/// Converts a full Indicator to a LightIndicator
/// </summary>
public static LightIndicator FromIndicator(Indicator indicator)
{
return new LightIndicator(indicator.Name, indicator.Type)
{
SignalType = indicator.SignalType,
MinimumHistory = indicator.MinimumHistory,
Period = indicator.Period,
FastPeriods = indicator.FastPeriods,
SlowPeriods = indicator.SlowPeriods,
SignalPeriods = indicator.SignalPeriods,
Multiplier = indicator.Multiplier,
SmoothPeriods = indicator.SmoothPeriods,
StochPeriods = indicator.StochPeriods,
CyclePeriods = indicator.CyclePeriods
};
}
/// <summary>
/// Converts a LightIndicator back to a full Indicator
/// </summary>
public Indicator ToIndicator()
{
return new Indicator(Name, Type)
{
SignalType = SignalType,
MinimumHistory = MinimumHistory,
Period = Period,
FastPeriods = FastPeriods,
SlowPeriods = SlowPeriods,
SignalPeriods = SignalPeriods,
Multiplier = Multiplier,
SmoothPeriods = SmoothPeriods,
StochPeriods = StochPeriods,
CyclePeriods = CyclePeriods
};
}
}

View File

@@ -2,8 +2,10 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Managing.Core;
using Managing.Domain.Candles;
using Orleans;
using static Managing.Common.Enums;
[GenerateSerializer]
public class LightSignal : ValueObject
{
public LightSignal(Ticker ticker, TradeDirection direction, Confidence confidence, Candle candle, DateTime date,
@@ -24,17 +26,40 @@ public class LightSignal : ValueObject
$"{indicatorName}-{indicatorType}-{direction}-{ticker}-{candle?.Close.ToString(CultureInfo.InvariantCulture)}-{date:yyyyMMdd-HHmmss}";
}
[Id(0)]
[Required] public SignalStatus Status { get; set; }
[Id(1)]
[Required] public TradeDirection Direction { get; }
[Id(2)]
[Required] public Confidence Confidence { get; set; }
[Id(3)]
[Required] public Timeframe Timeframe { get; }
[Id(4)]
[Required] public DateTime Date { get; private set; }
[Id(5)]
[Required] public Candle Candle { get; }
[Id(6)]
[Required] public string Identifier { get; }
[Id(7)]
[Required] public Ticker Ticker { get; }
[Id(8)]
[Required] public TradingExchanges Exchange { get; set; }
[Id(9)]
[Required] public IndicatorType IndicatorType { get; set; }
[Id(10)]
[Required] public SignalType SignalType { get; set; }
[Id(11)]
[Required] public string IndicatorName { get; set; }
protected override IEnumerable<object> GetEqualityComponents()

View File

@@ -1,10 +1,12 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Managing.Domain.Users;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Domain.Trades
{
[GenerateSerializer]
public class Position
{
public Position(string identifier, string accountName, TradeDirection originDirection, Ticker ticker,
@@ -21,28 +23,53 @@ namespace Managing.Domain.Trades
User = user;
}
[Id(0)]
[Required] public string AccountName { get; set; }
[Id(1)]
[Required] public DateTime Date { get; set; }
[Id(2)]
[Required] public TradeDirection OriginDirection { get; set; }
[Id(3)]
[Required] public Ticker Ticker { get; set; }
[Id(4)]
[Required] public LightMoneyManagement MoneyManagement { get; set; }
[Id(5)]
[Required] [JsonPropertyName("Open")] public Trade Open { get; set; }
[Id(6)]
[Required]
[JsonPropertyName("StopLoss")]
public Trade StopLoss { get; set; }
[Id(7)]
[Required]
[JsonPropertyName("TakeProfit1")]
public Trade TakeProfit1 { get; set; }
[Id(8)]
[JsonPropertyName("TakeProfit2")] public Trade TakeProfit2 { get; set; }
[Id(9)]
[JsonPropertyName("ProfitAndLoss")] public ProfitAndLoss ProfitAndLoss { get; set; }
[Id(10)]
[Required] public PositionStatus Status { get; set; }
[Id(11)]
public string SignalIdentifier { get; set; }
[Id(12)]
[Required] public string Identifier { get; set; }
[Id(13)]
[Required] public PositionInitiator Initiator { get; set; }
[Id(14)]
[Required] public User User { get; set; }
public bool IsFinished()

View File

@@ -1,13 +1,18 @@
using static Managing.Common.Enums;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Domain.Trades
{
[GenerateSerializer]
public sealed class ProfitAndLoss
{
[Id(0)]
public decimal Realized { get; set; }
[Id(1)]
public decimal Net { get; set; }
[Id(2)]
public decimal AverageOpenPrice { get; private set; }
private const decimal _multiplier = 100000;

View File

@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Orleans;
using static Managing.Common.Enums;
namespace Managing.Domain.Trades
{
[GenerateSerializer]
public class Trade
{
public Trade(DateTime date, TradeDirection direction, TradeStatus status, TradeType tradeType, Ticker ticker,
@@ -21,16 +23,37 @@ namespace Managing.Domain.Trades
Fee = 0;
}
[Id(0)]
[Required] public decimal Fee { get; set; }
[Id(1)]
[Required] public DateTime Date { get; set; }
[Id(2)]
[Required] public TradeDirection Direction { get; set; }
[Id(3)]
[Required] public TradeStatus Status { get; set; }
[Id(4)]
[Required] public TradeType TradeType { get; set; }
[Id(5)]
[Required] public Ticker Ticker { get; set; }
[Id(6)]
[Required] public decimal Quantity { get; set; }
[Id(7)]
[Required] public decimal Price { get; set; }
[Id(8)]
[Required] public decimal Leverage { get; set; }
[Id(9)]
[Required] public string ExchangeOrderId { get; set; }
[Id(10)]
[Required] public string Message { get; set; }
public void SetStatus(TradeStatus status)

View File

@@ -1,12 +1,23 @@
using Managing.Domain.Accounts;
using Orleans;
namespace Managing.Domain.Users;
[GenerateSerializer]
public class User
{
[Id(0)]
public string Name { get; set; }
[Id(1)]
public List<Account> Accounts { get; set; }
[Id(2)]
public string AgentName { get; set; }
[Id(3)]
public string AvatarUrl { get; set; }
[Id(4)]
public string TelegramChannel { get; set; }
}

View File

@@ -12,7 +12,7 @@
<PackageReference Include="FTX.Net" Version="1.0.16" />
<PackageReference Include="KrakenExchange.Net" Version="4.6.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -6,14 +6,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.15.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1"/>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2"/>
<PackageReference Include="Discord.Net" Version="3.15.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj"/>
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj" />
</ItemGroup>
</Project>