Add synthApi (#27)

* Add synthApi

* Put confidence for Synth proba

* Update the code

* Update readme

* Fix bootstraping

* fix github build

* Update the endpoints for scenario

* Add scenario and update backtest modal

* Update bot modal

* Update interfaces for synth

* add synth to backtest

* Add Kelly criterion and better signal

* Update signal confidence

* update doc

* save leaderboard and prediction

* Update nswag to generate ApiClient in the correct path

* Unify the trading modal

* Save miner and prediction

* Update messaging and block new signal until position not close when flipping off

* Rename strategies to indicators

* Update doc

* Update chart + add signal name

* Fix signal direction

* Update docker webui

* remove crypto npm

* Clean
This commit is contained in:
Oda
2025-07-03 00:13:42 +07:00
committed by GitHub
parent 453806356d
commit a547c4a040
103 changed files with 9916 additions and 810 deletions

View File

@@ -1,9 +1,12 @@
using Managing.Application.Abstractions;
using Managing.Api.Models.Requests;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
@@ -123,7 +126,7 @@ public class BacktestController : BaseController
return BadRequest("Either scenario name or scenario object is required");
}
if (string.IsNullOrEmpty(request.MoneyManagementName) && request.MoneyManagement == null)
if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null)
{
return BadRequest("Either money management name or money management object is required");
}
@@ -136,26 +139,58 @@ public class BacktestController : BaseController
// Get money management
MoneyManagement moneyManagement;
if (!string.IsNullOrEmpty(request.MoneyManagementName))
if (!string.IsNullOrEmpty(request.Config.MoneyManagementName))
{
moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
moneyManagement =
await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagementName);
if (moneyManagement == null)
return BadRequest("Money management not found");
}
else
{
moneyManagement = request.MoneyManagement;
moneyManagement = Map(request.Config.MoneyManagement);
moneyManagement?.FormatPercentage();
}
// Update config with money management - TradingBot will handle scenario loading
// Handle scenario - either from ScenarioRequest or ScenarioName
Scenario scenario = null;
if (request.Config.Scenario != null)
{
// Convert ScenarioRequest to Scenario domain object
scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod)
{
User = user
};
// Convert IndicatorRequest objects to Indicator domain objects
foreach (var indicatorRequest in request.Config.Scenario.Indicators)
{
var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
{
SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory,
Period = indicatorRequest.Period,
FastPeriods = indicatorRequest.FastPeriods,
SlowPeriods = indicatorRequest.SlowPeriods,
SignalPeriods = indicatorRequest.SignalPeriods,
Multiplier = indicatorRequest.Multiplier,
SmoothPeriods = indicatorRequest.SmoothPeriods,
StochPeriods = indicatorRequest.StochPeriods,
CyclePeriods = indicatorRequest.CyclePeriods,
User = user
};
scenario.AddIndicator(indicator);
}
}
// Convert TradingBotConfigRequest to TradingBotConfig for backtest
var backtestConfig = new TradingBotConfig
{
AccountName = request.Config.AccountName,
MoneyManagement = moneyManagement,
Ticker = request.Config.Ticker,
ScenarioName = request.Config.ScenarioName,
Scenario = request.Config.Scenario,
Scenario = scenario, // Use the converted scenario object
Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.WatchOnly,
BotTradingBalance = request.Balance,
@@ -165,10 +200,14 @@ public class BacktestController : BaseController
MaxLossStreak = request.Config.MaxLossStreak,
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
FlipPosition = request.Config.FlipPosition,
FlipPosition = request.Config.BotType == BotType.FlippingBot, // Computed based on BotType
Name = request.Config.Name ??
$"Backtest-{request.Config.ScenarioName ?? request.Config.Scenario?.Name ?? "Custom"}-{DateTime.UtcNow:yyyyMMdd-HHmmss}",
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable,
UseSynthApi = request.Config.UseSynthApi,
UseForPositionSizing = request.Config.UseForPositionSizing,
UseForSignalFiltering = request.Config.UseForSignalFiltering,
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss
};
switch (request.Config.BotType)
@@ -208,6 +247,18 @@ public class BacktestController : BaseController
await _hubContext.Clients.All.SendAsync("BacktestsSubscription", backtesting);
}
}
public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest)
{
return new MoneyManagement
{
Name = moneyManagementRequest.Name,
StopLoss = moneyManagementRequest.StopLoss,
TakeProfit = moneyManagementRequest.TakeProfit,
Leverage = moneyManagementRequest.Leverage,
Timeframe = moneyManagementRequest.Timeframe
};
}
}
/// <summary>
@@ -216,9 +267,9 @@ public class BacktestController : BaseController
public class RunBacktestRequest
{
/// <summary>
/// The trading bot configuration to use for the backtest
/// The trading bot configuration request to use for the backtest
/// </summary>
public TradingBotConfig Config { get; set; }
public TradingBotConfigRequest Config { get; set; }
/// <summary>
/// The start date for the backtest
@@ -244,14 +295,4 @@ public class RunBacktestRequest
/// Whether to save the backtest results
/// </summary>
public bool Save { get; set; } = false;
/// <summary>
/// The name of the money management to use (optional if MoneyManagement is provided)
/// </summary>
public string? MoneyManagementName { get; set; }
/// <summary>
/// The money management details (optional if MoneyManagementName is provided)
/// </summary>
public MoneyManagement? MoneyManagement { get; set; }
}

View File

@@ -30,6 +30,9 @@ public abstract class BaseController : ControllerBase
throw new Exception("User not found for this token");
}
throw new Exception("Not identity assigned to this token");
}
}
}

View File

@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Managing.Api.Models.Requests;
using Managing.Api.Models.Responses;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
@@ -7,6 +7,8 @@ using Managing.Application.ManageBot.Commands;
using Managing.Common;
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Managing.Domain.Trades;
using MediatR;
using Microsoft.AspNetCore.Authorization;
@@ -117,24 +119,37 @@ public class BotController : BaseController
return Forbid("You don't have permission to start a bot with this account");
}
// Trigger error if money management is not provided
if (string.IsNullOrEmpty(request.MoneyManagementName) && request.Config.MoneyManagement == null)
// Validate that either money management name or object is provided
if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null)
{
return BadRequest("Money management name or money management object is required");
return BadRequest("Either money management name or money management object is required");
}
var user = await GetUser();
// Get money management if name is provided
MoneyManagement moneyManagement = request.Config.MoneyManagement;
if (!string.IsNullOrEmpty(request.MoneyManagementName))
// Get money management - either by name lookup or use provided object
MoneyManagement moneyManagement;
if (!string.IsNullOrEmpty(request.Config.MoneyManagementName))
{
moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
moneyManagement =
await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagementName);
if (moneyManagement == null)
{
return BadRequest("Money management not found");
}
}
else
{
moneyManagement = Map(request.Config.MoneyManagement);
// Format percentage values if using custom money management
moneyManagement?.FormatPercentage();
// Ensure user is set for custom money management
if (moneyManagement != null)
{
moneyManagement.User = user;
}
}
// Validate initialTradingBalance
if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
@@ -167,13 +182,45 @@ public class BotController : BaseController
return BadRequest("CloseEarlyWhenProfitable can only be enabled when MaxPositionTimeHours is set");
}
// Update the config with final money management
// Handle scenario - either from ScenarioRequest or ScenarioName
Scenario scenario = null;
if (request.Config.Scenario != null)
{
// Convert ScenarioRequest to Scenario domain object
scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod)
{
User = user
};
// Convert IndicatorRequest objects to Indicator domain objects
foreach (var indicatorRequest in request.Config.Scenario.Indicators)
{
var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
{
SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory,
Period = indicatorRequest.Period,
FastPeriods = indicatorRequest.FastPeriods,
SlowPeriods = indicatorRequest.SlowPeriods,
SignalPeriods = indicatorRequest.SignalPeriods,
Multiplier = indicatorRequest.Multiplier,
SmoothPeriods = indicatorRequest.SmoothPeriods,
StochPeriods = indicatorRequest.StochPeriods,
CyclePeriods = indicatorRequest.CyclePeriods,
User = user
};
scenario.AddIndicator(indicator);
}
}
// Map the request to the full TradingBotConfig
var config = new TradingBotConfig
{
AccountName = request.Config.AccountName,
MoneyManagement = moneyManagement,
Ticker = request.Config.Ticker,
ScenarioName = request.Config.ScenarioName,
Scenario = scenario, // Use the converted scenario object
ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided
Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly,
BotTradingBalance = request.Config.BotTradingBalance,
@@ -182,10 +229,15 @@ public class BotController : BaseController
MaxLossStreak = request.Config.MaxLossStreak,
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable,
UseSynthApi = request.Config.UseSynthApi,
UseForPositionSizing = request.Config.UseForPositionSizing,
UseForSignalFiltering = request.Config.UseForSignalFiltering,
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
// Set computed/default properties
IsForBacktest = false,
FlipPosition = request.Config.BotType == BotType.FlippingBot,
Name = request.Config.Name,
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
Name = request.Config.Name
};
var result = await _mediator.Send(new StartBotCommand(config, request.Config.Name, user));
@@ -200,6 +252,7 @@ public class BotController : BaseController
}
}
/// <summary>
/// Stops a bot specified by type and name.
/// </summary>
@@ -614,7 +667,7 @@ public class BotController : BaseController
// Get the existing bot to ensure it exists and get current config
var bots = _botService.GetActiveBots();
var existingBot = bots.FirstOrDefault(b => b.Identifier == request.Identifier);
if (existingBot == null)
{
return NotFound($"Bot with identifier '{request.Identifier}' not found");
@@ -630,9 +683,9 @@ public class BotController : BaseController
}
// If the bot name is being changed, check for conflicts
var isNameChanging = !string.IsNullOrEmpty(request.Config.Name) &&
var isNameChanging = !string.IsNullOrEmpty(request.Config.Name) &&
request.Config.Name != request.Identifier;
if (isNameChanging)
{
// Check if new name already exists
@@ -643,31 +696,36 @@ public class BotController : BaseController
}
}
// Validate the money management if provided
if (request.Config.MoneyManagement != null)
// Validate and get the money management
MoneyManagement moneyManagement = null;
if (!string.IsNullOrEmpty(request.MoneyManagementName))
{
// Check if the money management belongs to the user
var userMoneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagement.Name);
if (userMoneyManagement != null && userMoneyManagement.User?.Name != user.Name)
{
return Forbid("You don't have permission to use this money management");
}
}
else if (!string.IsNullOrEmpty(request.MoneyManagementName))
{
// If MoneyManagement is null but MoneyManagementName is provided, load it
var moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
// Load money management by name
moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName);
if (moneyManagement == null)
{
return BadRequest($"Money management '{request.MoneyManagementName}' not found");
}
if (moneyManagement.User?.Name != user.Name)
{
return Forbid("You don't have permission to use this money management");
}
request.Config.MoneyManagement = moneyManagement;
}
else if (request.MoneyManagement != null)
{
// Use provided money management object
moneyManagement = request.MoneyManagement;
// Format percentage values if using custom money management
moneyManagement.FormatPercentage();
// Ensure user is set for custom money management
moneyManagement.User = user;
}
else
{
// Use existing bot's money management if no new one is provided
moneyManagement = existingBot.Config.MoneyManagement;
}
// Validate CloseEarlyWhenProfitable requires MaxPositionTimeHours
@@ -676,27 +734,85 @@ public class BotController : BaseController
return BadRequest("CloseEarlyWhenProfitable requires MaxPositionTimeHours to be set");
}
// Handle scenario - either from ScenarioRequest or ScenarioName
Scenario scenarioForUpdate = null;
if (request.Config.Scenario != null)
{
// Convert ScenarioRequest to Scenario domain object
scenarioForUpdate = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod)
{
User = user
};
// Convert IndicatorRequest objects to Indicator domain objects
foreach (var indicatorRequest in request.Config.Scenario.Indicators)
{
var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
{
SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory,
Period = indicatorRequest.Period,
FastPeriods = indicatorRequest.FastPeriods,
SlowPeriods = indicatorRequest.SlowPeriods,
SignalPeriods = indicatorRequest.SignalPeriods,
Multiplier = indicatorRequest.Multiplier,
SmoothPeriods = indicatorRequest.SmoothPeriods,
StochPeriods = indicatorRequest.StochPeriods,
CyclePeriods = indicatorRequest.CyclePeriods,
User = user
};
scenarioForUpdate.AddIndicator(indicator);
}
}
// Map the request to the full TradingBotConfig
var updatedConfig = new TradingBotConfig
{
AccountName = request.Config.AccountName,
MoneyManagement = moneyManagement,
Ticker = request.Config.Ticker,
Scenario = scenarioForUpdate, // Use the converted scenario object
ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided
Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly,
BotTradingBalance = request.Config.BotTradingBalance,
BotType = request.Config.BotType,
CooldownPeriod = request.Config.CooldownPeriod,
MaxLossStreak = request.Config.MaxLossStreak,
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable,
UseSynthApi = request.Config.UseSynthApi,
UseForPositionSizing = request.Config.UseForPositionSizing,
UseForSignalFiltering = request.Config.UseForSignalFiltering,
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
// Set computed/default properties
IsForBacktest = false,
FlipPosition = request.Config.BotType == BotType.FlippingBot,
Name = request.Config.Name
};
// Update the bot configuration using the enhanced method
var success = await _botService.UpdateBotConfiguration(request.Identifier, request.Config);
var success = await _botService.UpdateBotConfiguration(request.Identifier, updatedConfig);
if (success)
{
var finalBotName = isNameChanging ? request.Config.Name : request.Identifier;
await _hubContext.Clients.All.SendAsync("SendNotification",
$"Bot {finalBotName} configuration updated successfully by {user.Name}." +
(isNameChanging ? $" (renamed from {request.Identifier})" : ""), "Info");
await NotifyBotSubscriberAsync();
return Ok(isNameChanging
return Ok(isNameChanging
? $"Bot configuration updated successfully and renamed to '{request.Config.Name}'"
: "Bot configuration updated successfully");
}
else
{
return BadRequest("Failed to update bot configuration. " +
(isNameChanging ? "The new name might already be in use." : ""));
(isNameChanging ? "The new name might already be in use." : ""));
}
}
catch (Exception ex)
@@ -705,6 +821,18 @@ public class BotController : BaseController
return StatusCode(500, $"Error updating bot configuration: {ex.Message}");
}
}
public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest)
{
return new MoneyManagement
{
Name = moneyManagementRequest.Name,
StopLoss = moneyManagementRequest.StopLoss,
TakeProfit = moneyManagementRequest.TakeProfit,
Leverage = moneyManagementRequest.Leverage,
Timeframe = moneyManagementRequest.Timeframe
};
}
}
/// <summary>
@@ -745,35 +873,7 @@ public class ClosePositionRequest
public class StartBotRequest
{
/// <summary>
/// The trading bot configuration
/// The trading bot configuration request with primary properties
/// </summary>
public TradingBotConfig Config { get; set; }
/// <summary>
/// Optional money management name (if not included in Config.MoneyManagement)
/// </summary>
public string? MoneyManagementName { get; set; }
}
/// <summary>
/// Request model for updating bot configuration
/// </summary>
public class UpdateBotConfigRequest
{
/// <summary>
/// The unique identifier of the bot to update
/// </summary>
[Required]
public string Identifier { get; set; }
/// <summary>
/// The new trading bot configuration
/// </summary>
[Required]
public TradingBotConfig Config { get; set; }
/// <summary>
/// Optional: Money management name to load if Config.MoneyManagement is null
/// </summary>
public string? MoneyManagementName { get; set; }
public TradingBotConfigRequest Config { get; set; }
}

View File

@@ -1,4 +1,5 @@
using Managing.Application.Abstractions;
using Managing.Api.Models.Responses;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
@@ -39,10 +40,12 @@ public class ScenarioController : BaseController
/// </summary>
/// <returns>A list of scenarios.</returns>
[HttpGet]
public async Task<ActionResult<IEnumerable<Scenario>>> GetScenarios()
public async Task<ActionResult<IEnumerable<ScenarioViewModel>>> GetScenarios()
{
var user = await GetUser();
return Ok(_scenarioService.GetScenariosByUser(user));
var scenarios = _scenarioService.GetScenariosByUser(user);
var scenarioViewModels = scenarios.Select(MapToScenarioViewModel);
return Ok(scenarioViewModels);
}
/// <summary>
@@ -52,11 +55,13 @@ public class ScenarioController : BaseController
/// <param name="strategies">A list of strategy names to include in the scenario.</param>
/// <returns>The created scenario.</returns>
[HttpPost]
public async Task<ActionResult<Scenario>> CreateScenario(string name, List<string> strategies,
public async Task<ActionResult<ScenarioViewModel>> CreateScenario(string name, List<string> strategies,
int? loopbackPeriod = null)
{
var user = await GetUser();
return Ok(_scenarioService.CreateScenarioForUser(user, name, strategies, loopbackPeriod));
var scenario = _scenarioService.CreateScenarioForUser(user, name, strategies, loopbackPeriod);
var scenarioViewModel = MapToScenarioViewModel(scenario);
return Ok(scenarioViewModel);
}
/// <summary>
@@ -85,10 +90,12 @@ public class ScenarioController : BaseController
/// <returns>A list of strategies.</returns>
[HttpGet]
[Route("indicator")]
public async Task<ActionResult<IEnumerable<Indicator>>> GetIndicators()
public async Task<ActionResult<IEnumerable<IndicatorViewModel>>> GetIndicators()
{
var user = await GetUser();
return Ok(_scenarioService.GetIndicatorsByUser(user));
var indicators = _scenarioService.GetIndicatorsByUser(user);
var indicatorViewModels = indicators.Select(MapToIndicatorViewModel);
return Ok(indicatorViewModels);
}
/// <summary>
@@ -107,7 +114,7 @@ public class ScenarioController : BaseController
/// <returns>The created indicator.</returns>
[HttpPost]
[Route("indicator")]
public async Task<ActionResult<Indicator>> CreateIndicator(
public async Task<ActionResult<IndicatorViewModel>> CreateIndicator(
IndicatorType indicatorType,
string name,
int? period = null,
@@ -120,7 +127,7 @@ public class ScenarioController : BaseController
int? cyclePeriods = null)
{
var user = await GetUser();
return Ok(_scenarioService.CreateIndicatorForUser(
var indicator = _scenarioService.CreateIndicatorForUser(
user,
indicatorType,
name,
@@ -131,7 +138,9 @@ public class ScenarioController : BaseController
multiplier,
stochPeriods,
smoothPeriods,
cyclePeriods));
cyclePeriods);
var indicatorViewModel = MapToIndicatorViewModel(indicator);
return Ok(indicatorViewModel);
}
/// <summary>
@@ -176,4 +185,35 @@ public class ScenarioController : BaseController
smoothPeriods,
cyclePeriods));
}
private static ScenarioViewModel MapToScenarioViewModel(Scenario scenario)
{
return new ScenarioViewModel
{
Name = scenario.Name,
LoopbackPeriod = scenario.LoopbackPeriod,
UserName = scenario.User?.Name,
Indicators = scenario.Indicators?.Select(MapToIndicatorViewModel).ToList() ?? new List<IndicatorViewModel>()
};
}
private static IndicatorViewModel MapToIndicatorViewModel(Indicator indicator)
{
return new IndicatorViewModel
{
Name = indicator.Name,
Type = 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,
UserName = indicator.User?.Name
};
}
}

View File

@@ -0,0 +1,73 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for indicator configuration without user information
/// </summary>
public class IndicatorRequest
{
/// <summary>
/// The name of the indicator
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// The type of indicator
/// </summary>
[Required]
public IndicatorType Type { get; set; }
/// <summary>
/// The signal type for this indicator
/// </summary>
[Required]
public SignalType SignalType { get; set; }
/// <summary>
/// Minimum history required for this indicator
/// </summary>
public int MinimumHistory { get; set; }
/// <summary>
/// Period parameter for the indicator
/// </summary>
public int? Period { get; set; }
/// <summary>
/// Fast periods parameter for indicators like MACD
/// </summary>
public int? FastPeriods { get; set; }
/// <summary>
/// Slow periods parameter for indicators like MACD
/// </summary>
public int? SlowPeriods { get; set; }
/// <summary>
/// Signal periods parameter for indicators like MACD
/// </summary>
public int? SignalPeriods { get; set; }
/// <summary>
/// Multiplier parameter for indicators like SuperTrend
/// </summary>
public double? Multiplier { get; set; }
/// <summary>
/// Smooth periods parameter
/// </summary>
public int? SmoothPeriods { get; set; }
/// <summary>
/// Stochastic periods parameter
/// </summary>
public int? StochPeriods { get; set; }
/// <summary>
/// Cycle periods parameter
/// </summary>
public int? CyclePeriods { get; set; }
}

View File

@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
using Managing.Common;
namespace Managing.Api.Models.Requests;
public class MoneyManagementRequest
{
[Required] public string Name { get; set; }
[Required] public Enums.Timeframe Timeframe { get; set; }
[Required] public decimal StopLoss { get; set; }
[Required] public decimal TakeProfit { get; set; }
[Required] public decimal Leverage { get; set; }
}

View File

@@ -1,15 +1,49 @@
using static Managing.Common.Enums;
using Managing.Domain.MoneyManagements;
namespace Managing.Api.Models.Requests
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for running a backtest
/// </summary>
public class RunBacktestRequest
{
public class RunBacktestRequest
{
public TradingExchanges Exchange { get; set; }
public BotType BotType { get; set; }
public Ticker Ticker { get; set; }
public Timeframe Timeframe { get; set; }
public RiskLevel RiskLevel { get; set; }
public bool WatchOnly { get; set; }
public int Days { get; set; }
}
/// <summary>
/// The trading bot configuration request to use for the backtest
/// </summary>
public TradingBotConfigRequest Config { get; set; }
/// <summary>
/// The start date for the backtest
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
/// The end date for the backtest
/// </summary>
public DateTime EndDate { get; set; }
/// <summary>
/// The starting balance for the backtest
/// </summary>
public decimal Balance { get; set; }
/// <summary>
/// Whether to only watch the backtest without executing trades
/// </summary>
public bool WatchOnly { get; set; } = false;
/// <summary>
/// Whether to save the backtest results
/// </summary>
public bool Save { get; set; } = false;
/// <summary>
/// The name of the money management to use (optional if MoneyManagement is provided)
/// </summary>
public string? MoneyManagementName { get; set; }
/// <summary>
/// The money management details (optional if MoneyManagementName is provided)
/// </summary>
public MoneyManagement? MoneyManagement { get; set; }
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for scenario configuration without user information
/// </summary>
public class ScenarioRequest
{
/// <summary>
/// The name of the scenario
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// List of indicator configurations for this scenario
/// </summary>
[Required]
public List<IndicatorRequest> Indicators { get; set; } = new();
/// <summary>
/// The loopback period for the scenario
/// </summary>
public int? LoopbackPeriod { get; set; }
}

View File

@@ -1,31 +1,16 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests
{
/// <summary>
/// Request model for starting a bot
/// </summary>
public class StartBotRequest
{
[Required] public BotType BotType { get; set; }
[Required] public string BotName { get; set; }
[Required] public Ticker Ticker { get; set; }
[Required] public Timeframe Timeframe { get; set; }
[Required] public bool IsForWatchOnly { get; set; }
[Required] public string Scenario { get; set; }
[Required] public string AccountName { get; set; }
[Required] public string MoneyManagementName { get; set; }
/// <summary>
/// Initial trading balance in USD for the bot
/// The trading bot configuration request with primary properties
/// </summary>
[Required]
[Range(10.00, double.MaxValue, ErrorMessage = "Initial trading balance must be greater than ten")]
public decimal InitialTradingBalance { get; set; }
/// <summary>
/// Cooldown period in minutes between trades
/// </summary>
[Required]
[Range(1, 1440, ErrorMessage = "Cooldown period must be between 1 and 1440 minutes (24 hours)")]
public decimal CooldownPeriod { get; set; } = 1; // Default to 1 minute if not specified
public TradingBotConfigRequest Config { get; set; }
}
}

View File

@@ -0,0 +1,119 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Simplified trading bot configuration request with only primary properties
/// </summary>
public class TradingBotConfigRequest
{
/// <summary>
/// The account name to use for trading
/// </summary>
[Required]
public string AccountName { get; set; }
/// <summary>
/// The ticker/symbol to trade
/// </summary>
[Required]
public Ticker Ticker { get; set; }
/// <summary>
/// The timeframe for trading decisions
/// </summary>
[Required]
public Timeframe Timeframe { get; set; }
/// <summary>
/// Whether this bot is for watching only (no actual trading)
/// </summary>
[Required]
public bool IsForWatchingOnly { get; set; }
/// <summary>
/// The initial trading balance for the bot
/// </summary>
[Required]
public decimal BotTradingBalance { get; set; }
/// <summary>
/// The type of bot (SimpleBot, ScalpingBot, FlippingBot)
/// </summary>
[Required]
public BotType BotType { get; set; }
/// <summary>
/// The name/identifier for this bot
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// Cooldown period between trades (in candles)
/// </summary>
[Required]
public int CooldownPeriod { get; set; }
/// <summary>
/// Maximum consecutive losses before stopping the bot
/// </summary>
[Required]
public int MaxLossStreak { get; set; }
/// <summary>
/// The scenario configuration (takes precedence over ScenarioName)
/// </summary>
public ScenarioRequest? Scenario { get; set; }
/// <summary>
/// The scenario name to load from database (only used when Scenario is not provided)
/// </summary>
public string? ScenarioName { get; set; }
/// <summary>
/// The money management name to load from database (only used when MoneyManagement is not provided)
/// </summary>
public string? MoneyManagementName { get; set; }
/// <summary>
/// The money management object to use for the bot
/// </summary>
public MoneyManagementRequest? MoneyManagement { get; set; }
/// <summary>
/// Maximum time in hours that a position can remain open before being automatically closed
/// </summary>
public decimal? MaxPositionTimeHours { get; set; }
/// <summary>
/// Whether to close positions early when they become profitable
/// </summary>
public bool CloseEarlyWhenProfitable { get; set; } = false;
/// <summary>
/// Whether to only flip positions when the current position is in profit
/// </summary>
public bool FlipOnlyWhenInProfit { get; set; } = true;
/// <summary>
/// Whether to use Synth API for predictions and risk assessment
/// </summary>
public bool UseSynthApi { get; set; } = false;
/// <summary>
/// Whether to use Synth predictions for position sizing adjustments
/// </summary>
public bool UseForPositionSizing { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for signal filtering
/// </summary>
public bool UseForSignalFiltering { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary>
public bool UseForDynamicStopLoss { get; set; } = true;
}

View File

@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using Managing.Domain.MoneyManagements;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for updating bot configuration
/// </summary>
public class UpdateBotConfigRequest
{
/// <summary>
/// The unique identifier of the bot to update
/// </summary>
[Required]
public string Identifier { get; set; }
/// <summary>
/// The new trading bot configuration request
/// </summary>
[Required]
public TradingBotConfigRequest Config { get; set; }
/// <summary>
/// Optional: Money management name to load from database (if MoneyManagement object is not provided)
/// </summary>
public string? MoneyManagementName { get; set; }
/// <summary>
/// Optional: Money management object for custom configurations (takes precedence over MoneyManagementName)
/// </summary>
public MoneyManagement? MoneyManagement { get; set; }
}

View File

@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Responses;
public class IndicatorViewModel
{
[Required]
public string Name { get; set; } = string.Empty;
[Required]
public IndicatorType Type { get; set; }
[Required]
public SignalType SignalType { get; set; }
[Required]
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; }
[Required]
public string UserName { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace Managing.Api.Models.Responses;
public class ScenarioViewModel
{
[Required]
public string Name { get; set; } = string.Empty;
[Required]
public List<IndicatorViewModel> Indicators { get; set; } = new();
public int? LoopbackPeriod { get; set; }
[Required]
public string UserName { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,59 @@
using Managing.Domain.Synth.Models;
namespace Managing.Application.Abstractions.Repositories;
/// <summary>
/// Repository interface for Synth-related data operations
/// Provides MongoDB persistence for leaderboard and individual predictions data
/// </summary>
public interface ISynthRepository
{
/// <summary>
/// Gets cached leaderboard data by cache key
/// </summary>
/// <param name="cacheKey">The cache key to search for</param>
/// <returns>Cached leaderboard data if found, null otherwise</returns>
Task<SynthMinersLeaderboard?> GetLeaderboardAsync(string cacheKey);
/// <summary>
/// Saves leaderboard data to MongoDB
/// </summary>
/// <param name="leaderboard">The leaderboard data to save</param>
Task SaveLeaderboardAsync(SynthMinersLeaderboard leaderboard);
/// <summary>
/// Gets individual cached prediction data by asset, parameters, and miner UIDs
/// </summary>
/// <param name="asset">Asset symbol</param>
/// <param name="timeIncrement">Time increment in seconds</param>
/// <param name="timeLength">Time length in seconds</param>
/// <param name="minerUids">List of miner UIDs to get predictions for</param>
/// <param name="isBacktest">Whether this is backtest data</param>
/// <param name="signalDate">Signal date for backtest data</param>
/// <returns>List of cached individual predictions</returns>
Task<List<SynthPrediction>> GetIndividualPredictionsAsync(
string asset,
int timeIncrement,
int timeLength,
List<int> minerUids,
bool isBacktest,
DateTime? signalDate);
/// <summary>
/// Saves individual prediction data to MongoDB
/// </summary>
/// <param name="prediction">The individual prediction data to save</param>
Task SaveIndividualPredictionAsync(SynthPrediction prediction);
/// <summary>
/// Saves multiple individual predictions to MongoDB in batch
/// </summary>
/// <param name="predictions">The list of individual predictions to save</param>
Task SaveIndividualPredictionsAsync(List<SynthPrediction> predictions);
/// <summary>
/// Cleans up old cached data beyond the retention period
/// </summary>
/// <param name="retentionDays">Number of days to retain data</param>
Task CleanupOldDataAsync(int retentionDays = 30);
}

View File

@@ -0,0 +1,59 @@
using Managing.Domain.Synth.Models;
namespace Managing.Application.Abstractions.Services;
/// <summary>
/// Interface for communicating with the Synth API
/// </summary>
public interface ISynthApiClient
{
/// <summary>
/// Fetches the current leaderboard from Synth API
/// </summary>
/// <param name="config">Synth configuration containing API key and settings</param>
/// <returns>List of miners with their rankings and stats</returns>
Task<List<MinerInfo>> GetLeaderboardAsync(SynthConfiguration config);
/// <summary>
/// Fetches historical leaderboard data from Synth API for a specific time range
/// </summary>
/// <param name="startTime">Start time for historical data (ISO 8601 format)</param>
/// <param name="endTime">End time for historical data (ISO 8601 format)</param>
/// <param name="config">Synth configuration containing API key and settings</param>
/// <returns>List of miners with their historical rankings and stats</returns>
Task<List<MinerInfo>> GetHistoricalLeaderboardAsync(DateTime startTime, DateTime endTime, SynthConfiguration config);
/// <summary>
/// Fetches latest predictions from specified miners
/// </summary>
/// <param name="minerUids">List of miner UIDs to get predictions from</param>
/// <param name="asset">Asset symbol (e.g., "BTC", "ETH")</param>
/// <param name="timeIncrement">Time interval in seconds between each prediction point</param>
/// <param name="timeLength">Total prediction time length in seconds</param>
/// <param name="config">Synth configuration containing API key and settings</param>
/// <returns>List of predictions from the specified miners</returns>
Task<List<MinerPrediction>> GetMinerPredictionsAsync(
List<int> minerUids,
string asset,
int timeIncrement,
int timeLength,
SynthConfiguration config);
/// <summary>
/// Fetches historical predictions from specified miners for a specific time point
/// </summary>
/// <param name="minerUids">List of miner UIDs to get predictions from</param>
/// <param name="asset">Asset symbol (e.g., "BTC", "ETH")</param>
/// <param name="startTime">Start time for historical predictions (when the prediction was made)</param>
/// <param name="timeIncrement">Time interval in seconds between each prediction point</param>
/// <param name="timeLength">Total prediction time length in seconds</param>
/// <param name="config">Synth configuration containing API key and settings</param>
/// <returns>List of historical predictions from the specified miners</returns>
Task<List<MinerPrediction>> GetHistoricalMinerPredictionsAsync(
List<int> minerUids,
string asset,
DateTime startTime,
int timeIncrement,
int timeLength,
SynthConfiguration config);
}

View File

@@ -0,0 +1,109 @@
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Strategies;
using Managing.Domain.Synth.Models;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Services;
/// <summary>
/// Service interface for Synth prediction business logic and probability calculations
/// </summary>
public interface ISynthPredictionService
{
/// <summary>
/// Calculates the probability of price reaching a target within a specified time horizon
/// </summary>
/// <param name="asset">Asset symbol (e.g., "BTC", "ETH")</param>
/// <param name="currentPrice">Current market price</param>
/// <param name="targetPrice">Target price to reach</param>
/// <param name="timeHorizonSeconds">Time horizon in seconds</param>
/// <param name="isLongPosition">True for long positions (liquidation when price drops), false for short positions (liquidation when price rises)</param>
/// <param name="config">Synth configuration for this operation</param>
/// <returns>Probability as a decimal between 0.0 and 1.0</returns>
Task<decimal> GetProbabilityOfTargetPriceAsync(
string asset,
decimal currentPrice,
decimal targetPrice,
int timeHorizonSeconds,
bool isLongPosition,
SynthConfiguration config);
/// <summary>
/// Gets probabilities for multiple price thresholds at once
/// </summary>
/// <param name="asset">Asset symbol</param>
/// <param name="currentPrice">Current market price</param>
/// <param name="priceThresholds">Dictionary of threshold names to prices</param>
/// <param name="timeHorizonSeconds">Time horizon in seconds</param>
/// <param name="isLongPosition">True for long positions, false for short positions</param>
/// <param name="config">Synth configuration for this operation</param>
/// <param name="isBacktest">Parameter for backtest</param>
/// <param name="signalDate">Signal date</param>
/// <returns>Dictionary of threshold names to probabilities</returns>
Task<Dictionary<string, decimal>> GetMultipleThresholdProbabilitiesAsync(
string asset,
decimal currentPrice,
Dictionary<string, decimal> priceThresholds,
int timeHorizonSeconds,
bool isLongPosition,
SynthConfiguration config,
bool isBacktest,
DateTime signalDate);
/// <summary>
/// Clears cached predictions (useful for testing or forced refresh)
/// </summary>
void ClearCache();
/// <summary>
/// Clears cached predictions from MongoDB asynchronously
/// </summary>
Task ClearCacheAsync();
/// <summary>
/// Validates a trading signal using Synth predictions to check for adverse price movements
/// </summary>
/// <param name="signal">The trading signal containing ticker, direction, candle data, and other context</param>
/// <param name="currentPrice">Current market price (required)</param>
/// <param name="botConfig">Bot configuration with Synth settings</param>
/// <param name="isBacktest">Whether this is a backtest</param>
/// <param name="customThresholds">Custom probability thresholds for decision-making. If null, uses default thresholds.</param>
/// <returns>Comprehensive signal validation result including confidence, probabilities, and risk analysis</returns>
Task<SignalValidationResult> ValidateSignalAsync(Signal signal, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest, Dictionary<string, decimal> customThresholds = null);
/// <summary>
/// Performs risk assessment before opening a position
/// </summary>
/// <param name="ticker">Trading ticker</param>
/// <param name="direction">Position direction</param>
/// <param name="currentPrice">Current market price</param>
/// <param name="botConfig">Bot configuration with Synth settings</param>
/// <param name="isBacktest">Whether this is a backtest</param>
/// <returns>True if position should be allowed, false if blocked</returns>
Task<bool> AssessPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest);
/// <summary>
/// Monitors liquidation risk for an open position
/// </summary>
/// <param name="ticker">Trading ticker</param>
/// <param name="direction">Position direction</param>
/// <param name="currentPrice">Current market price</param>
/// <param name="liquidationPrice">Position liquidation price</param>
/// <param name="positionIdentifier">Position identifier for logging</param>
/// <param name="botConfig">Bot configuration with Synth settings</param>
/// <returns>Risk assessment result</returns>
Task<SynthRiskResult> MonitorPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig);
/// <summary>
/// Estimates liquidation price based on money management settings
/// </summary>
/// <param name="currentPrice">Current market price</param>
/// <param name="direction">Position direction</param>
/// <param name="moneyManagement">Money management settings</param>
/// <returns>Estimated liquidation price</returns>
decimal EstimateLiquidationPrice(decimal currentPrice, TradeDirection direction, MoneyManagement moneyManagement);
}

View File

@@ -1,7 +1,9 @@
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Scenarios;
using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Synth.Models;
using Managing.Domain.Trades;
using Managing.Infrastructure.Evm.Models.Privy;
using static Managing.Common.Enums;
@@ -37,4 +39,15 @@ public interface ITradingService
void UpdateStrategy(Indicator indicator);
Task<IEnumerable<Position>> GetBrokerPositions(Account account);
Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress);
// Synth API integration methods
Task<SignalValidationResult> ValidateSynthSignalAsync(Signal signal, decimal currentPrice,
TradingBotConfig botConfig,
bool isBacktest);
Task<bool> AssessSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest);
Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig);
}

View File

@@ -7,7 +7,7 @@ namespace Managing.Application.Abstractions;
public interface IBotService
{
void SaveOrUpdateBotBackup(User user, string identifier, BotType botType, BotStatus status, string data);
void SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, string data);
void AddSimpleBotToCache(IBot bot);
void AddTradingBotToCache(ITradingBot bot);
List<ITradingBot> GetActiveBots();
@@ -21,7 +21,7 @@ public interface IBotService
/// <param name="config">The trading bot configuration</param>
/// <returns>ITradingBot instance</returns>
ITradingBot CreateTradingBot(TradingBotConfig config);
/// <summary>
/// Creates a trading bot for backtesting using the unified TradingBot class
/// </summary>

View File

@@ -9,7 +9,7 @@ namespace Managing.Application.Abstractions
{
IEnumerable<Scenario> GetScenarios();
Scenario CreateScenario(string name, List<string> strategies, int? loopbackPeriod = 1);
IEnumerable<Indicator> GetStrategies();
IEnumerable<Indicator> GetIndicators();
bool DeleteStrategy(string name);
bool DeleteScenario(string name);
Scenario GetScenario(string name);

View File

@@ -14,7 +14,6 @@ namespace Managing.Application.Abstractions
{
TradingBotConfig Config { get; set; }
Account Account { get; set; }
HashSet<IIndicator> Indicators { get; set; }
FixedSizeQueue<Candle> OptimizedCandles { get; set; }
HashSet<Candle> Candles { get; set; }
HashSet<Signal> Signals { get; set; }
@@ -25,14 +24,12 @@ namespace Managing.Application.Abstractions
DateTime PreloadSince { get; set; }
int PreloadedCandlesCount { get; set; }
decimal Fee { get; set; }
Scenario Scenario { get; set; }
Task Run();
Task ToggleIsForWatchOnly();
int GetWinRate();
decimal GetProfitAndLoss();
decimal GetTotalFees();
void LoadIndicators(IEnumerable<IIndicator> indicators);
void LoadScenario(string scenarioName);
void LoadScenario(Scenario scenario);
void UpdateIndicatorsValues();

View File

@@ -183,19 +183,44 @@ namespace Managing.Application.Backtesting
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);
bot.Run();
currentCandle++;
// 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;
}
}
bot.Candles = new HashSet<Candle>(candles);
bot.UpdateIndicatorsValues();
_logger.LogInformation("Backtest processing completed. Calculating final results...");
var strategies = _scenarioService.GetStrategies();
var strategiesValues = GetStrategiesValues(strategies, candles);
bot.Candles = new HashSet<Candle>(candles);
// bot.UpdateIndicatorsValues();
var indicatorsValues = GetIndicatorsValues(bot.Config.Scenario.Indicators, candles);
var finalPnl = bot.GetProfitAndLoss();
var winRate = bot.GetWinRate();
@@ -230,7 +255,7 @@ namespace Managing.Application.Backtesting
WalletBalances = bot.WalletBalances.ToList(),
Statistics = stats,
OptimizedMoneyManagement = optimizedMoneyManagement,
StrategiesValues = AggregateValues(strategiesValues, bot.IndicatorsValues),
IndicatorsValues = AggregateValues(indicatorsValues, bot.IndicatorsValues),
Score = score
};
@@ -238,14 +263,14 @@ namespace Managing.Application.Backtesting
}
private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues(
Dictionary<IndicatorType, IndicatorsResultBase> strategiesValues,
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 strategy in strategiesValues)
foreach (var indicator in indicatorsValues)
{
// if (!botStrategiesValues.ContainsKey(strategy.Key))
// {
@@ -255,29 +280,29 @@ namespace Managing.Application.Backtesting
// result[strategy.Key] = botStrategiesValues[strategy.Key];
// }
result[strategy.Key] = strategy.Value;
result[indicator.Key] = indicator.Value;
}
return result;
}
private Dictionary<IndicatorType, IndicatorsResultBase> GetStrategiesValues(IEnumerable<Indicator> strategies,
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
List<Candle> candles)
{
var strategiesValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var fixedCandles = new FixedSizeQueue<Candle>(10000);
foreach (var candle in candles)
{
fixedCandles.Enqueue(candle);
}
foreach (var strategy in strategies)
foreach (var indicator in indicators)
{
try
{
var s = ScenarioHelpers.BuildIndicator(strategy, 10000);
var s = ScenarioHelpers.BuildIndicator(indicator, 10000);
s.Candles = fixedCandles;
strategiesValues[strategy.Type] = s.GetStrategyValues();
indicatorsValues[indicator.Type] = s.GetIndicatorValues();
}
catch (Exception e)
{
@@ -285,7 +310,7 @@ namespace Managing.Application.Backtesting
}
}
return strategiesValues;
return indicatorsValues;
}
public bool DeleteBacktest(string id)

View File

@@ -3,7 +3,6 @@ using Managing.Domain.Bots;
using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using static Managing.Common.Enums;
namespace Managing.Application.Bots
{
@@ -44,7 +43,7 @@ namespace Managing.Application.Bots
public override void SaveBackup()
{
var data = JsonConvert.SerializeObject(_workflow);
_botService.SaveOrUpdateBotBackup(User, Identifier, BotType.SimpleBot, Status, data);
_botService.SaveOrUpdateBotBackup(User, Identifier, Status, data);
}
public override void LoadBackup(BotBackup backup)

View File

@@ -7,7 +7,6 @@ using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
@@ -41,8 +40,6 @@ public class TradingBot : Bot, ITradingBot
public DateTime PreloadSince { get; set; }
public int PreloadedCandlesCount { get; set; }
public decimal Fee { get; set; }
public Scenario Scenario { get; set; }
public TradingBot(
IExchangeService exchangeService,
@@ -132,6 +129,9 @@ public class TradingBot : Bot, ITradingBot
public void LoadScenario(string scenarioName)
{
if (Config.Scenario != null)
return;
var scenario = TradingService.GetScenarioByName(scenarioName);
if (scenario == null)
{
@@ -140,7 +140,6 @@ public class TradingBot : Bot, ITradingBot
}
else
{
Scenario = scenario;
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
}
}
@@ -154,11 +153,15 @@ public class TradingBot : Bot, ITradingBot
}
else
{
Scenario = scenario;
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
}
}
public void LoadIndicators(Scenario scenario)
{
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
}
public void LoadIndicators(IEnumerable<IIndicator> indicators)
{
foreach (var strategy in indicators)
@@ -209,7 +212,7 @@ public class TradingBot : Bot, ITradingBot
}
UpdateWalletBalances();
if (OptimizedCandles.Count % 100 == 0) // Log every 10th execution
if (!Config.IsForBacktest) // Log every 10th execution
{
Logger.LogInformation($"Candle date : {OptimizedCandles.Last().Date:u}");
Logger.LogInformation($"Signals : {Signals.Count}");
@@ -223,7 +226,7 @@ public class TradingBot : Bot, ITradingBot
{
foreach (var strategy in Indicators)
{
IndicatorsValues[strategy.Type] = ((Indicator)strategy).GetStrategyValues();
IndicatorsValues[strategy.Type] = ((Indicator)strategy).GetIndicatorValues();
}
}
@@ -260,7 +263,10 @@ public class TradingBot : Bot, ITradingBot
private async Task UpdateSignals(FixedSizeQueue<Candle> candles)
{
var signal = TradingBox.GetSignal(candles.ToHashSet(), Indicators, Signals, Scenario.LoopbackPeriod);
// If position open and not flipped, do not update signals
if (!Config.FlipPosition && Positions.Any(p => !p.IsFinished())) return;
var signal = TradingBox.GetSignal(candles.ToHashSet(), Indicators, Signals, Config.Scenario.LoopbackPeriod);
if (signal == null) return;
signal.User = Account.User;
@@ -272,11 +278,39 @@ public class TradingBot : Bot, ITradingBot
if (Config.IsForWatchingOnly || (ExecutionCount < 1 && !Config.IsForBacktest))
signal.Status = SignalStatus.Expired;
Signals.Add(signal);
var signalText = $"{Config.ScenarioName} trigger a signal. Signal told you " +
$"to {signal.Direction} {Config.Ticker} on {Config.Timeframe}. The confidence in this signal is {signal.Confidence}. Identifier : {signal.Identifier}";
// Apply Synth-based signal filtering if enabled
if (Config.UseSynthApi)
{
var currentPrice = Config.IsForBacktest
? OptimizedCandles.Last().Close
: ExchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
var signalValidationResult = TradingService.ValidateSynthSignalAsync(signal, currentPrice, Config,
Config.IsForBacktest).GetAwaiter().GetResult();
if (signalValidationResult.Confidence == Confidence.None ||
signalValidationResult.Confidence == Confidence.Low ||
signalValidationResult.IsBlocked)
{
signal.Status = SignalStatus.Expired;
await LogInformation(
$"🚫 **Synth Signal Filter** - Signal {signal.Identifier} blocked by Synth risk assessment. Context : {signalValidationResult.ValidationContext}");
return;
}
else
{
signal.SetConfidence(signalValidationResult.Confidence);
signalText +=
$" and Synth risk assessment passed. Context : {signalValidationResult.ValidationContext}";
}
}
Signals.Add(signal);
Logger.LogInformation(signalText);
if (Config.IsForWatchingOnly && !Config.IsForBacktest && ExecutionCount > 0)
@@ -326,7 +360,8 @@ public class TradingBot : Bot, ITradingBot
date: position.Open.Date,
exchange: Account.Exchange,
indicatorType: IndicatorType.Stc, // Use a valid strategy type for recreated signals
signalType: SignalType.Signal
signalType: SignalType.Signal,
indicatorName: "RecreatedSignal"
);
// Since Signal identifier is auto-generated, we need to update our position
@@ -414,8 +449,6 @@ public class TradingBot : Bot, ITradingBot
{
try
{
Logger.LogInformation($"📊 **Position Update**\nUpdating position: `{positionForSignal.SignalIdentifier}`");
var position = Config.IsForBacktest
? positionForSignal
: TradingService.GetPositionByIdentifier(positionForSignal.Identifier);
@@ -624,6 +657,38 @@ public class TradingBot : Bot, ITradingBot
await OpenPosition(signal);
}
}
// Synth-based position monitoring for liquidation risk
if (Config.UseSynthApi && !Config.IsForBacktest &&
positionForSignal.Status == PositionStatus.Filled)
{
var currentPrice = ExchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
var riskResult = await TradingService.MonitorSynthPositionRiskAsync(
Config.Ticker,
positionForSignal.OriginDirection,
currentPrice,
positionForSignal.StopLoss.Price,
positionForSignal.Identifier,
Config);
if (riskResult != null && riskResult.ShouldWarn && !string.IsNullOrEmpty(riskResult.WarningMessage))
{
await LogWarning(riskResult.WarningMessage);
}
if (riskResult.ShouldAutoClose && !string.IsNullOrEmpty(riskResult.EmergencyMessage))
{
await LogWarning(riskResult.EmergencyMessage);
var signalForAutoClose =
Signals.FirstOrDefault(s => s.Identifier == positionForSignal.SignalIdentifier);
if (signalForAutoClose != null)
{
await CloseTrade(signalForAutoClose, positionForSignal, positionForSignal.StopLoss,
currentPrice, true);
}
}
}
}
catch (Exception ex)
{
@@ -784,6 +849,20 @@ public class TradingBot : Bot, ITradingBot
return false;
}
// Synth-based pre-trade risk assessment
if (Config.UseSynthApi)
{
var currentPrice = Config.IsForBacktest
? OptimizedCandles.Last().Close
: ExchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
if (!(await TradingService.AssessSynthPositionRiskAsync(Config.Ticker, signal.Direction, currentPrice,
Config, Config.IsForBacktest)))
{
return false;
}
}
// Check cooldown period and loss streak
return await CheckCooldownPeriod(signal) && await CheckLossStreak(signal);
}
@@ -1030,7 +1109,7 @@ public class TradingBot : Bot, ITradingBot
{
// Add PnL (could be positive or negative)
Config.BotTradingBalance += position.ProfitAndLoss.Realized;
Logger.LogInformation(
$"💰 **Balance Updated**\nNew bot trading balance: `${Config.BotTradingBalance:F2}`");
}
@@ -1150,7 +1229,7 @@ public class TradingBot : Bot, ITradingBot
public decimal GetTotalFees()
{
decimal totalFees = 0;
foreach (var position in Positions.Where(p => p.Open.Price > 0 && p.Open.Quantity > 0))
{
totalFees += CalculatePositionFees(position);
@@ -1167,22 +1246,22 @@ public class TradingBot : Bot, ITradingBot
private decimal CalculatePositionFees(Position position)
{
decimal fees = 0;
// Calculate position size in USD (leverage is already included in quantity calculation)
var positionSizeUsd = position.Open.Price * position.Open.Quantity;
// UI Fee: 0.1% of position size paid BOTH on opening AND closing
var uiFeeRate = 0.001m; // 0.1%
var uiFeeOpen = positionSizeUsd * uiFeeRate; // Fee paid on opening
var uiFeeClose = positionSizeUsd * uiFeeRate; // Fee paid on closing
var totalUiFees = uiFeeOpen + uiFeeClose; // Total: 0.2% of position size
var uiFeeOpen = positionSizeUsd * uiFeeRate; // Fee paid on opening
var uiFeeClose = positionSizeUsd * uiFeeRate; // Fee paid on closing
var totalUiFees = uiFeeOpen + uiFeeClose; // Total: 0.2% of position size
fees += totalUiFees;
// Network Fee: $0.50 for opening position only
// Closing is handled by oracle, so no network fee for closing
var networkFeeForOpening = 0.50m;
fees += networkFeeForOpening;
return fees;
}
@@ -1236,53 +1315,29 @@ public class TradingBot : Bot, ITradingBot
{
var data = new TradingBotBackup
{
Name = Name,
BotType = Config.BotType,
Config = Config,
Signals = Signals,
Positions = Positions,
Timeframe = Config.Timeframe,
Ticker = Config.Ticker,
ScenarioName = Config.ScenarioName,
AccountName = Config.AccountName,
IsForWatchingOnly = Config.IsForWatchingOnly,
WalletBalances = WalletBalances,
MoneyManagement = Config.MoneyManagement,
BotTradingBalance = Config.BotTradingBalance,
StartupTime = StartupTime,
CooldownPeriod = Config.CooldownPeriod,
MaxLossStreak = Config.MaxLossStreak,
MaxPositionTimeHours = Config.MaxPositionTimeHours ?? 0m,
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable,
StartupTime = StartupTime
};
BotService.SaveOrUpdateBotBackup(User, Identifier, Config.BotType, Status, JsonConvert.SerializeObject(data));
BotService.SaveOrUpdateBotBackup(User, Identifier, Status, JsonConvert.SerializeObject(data));
}
public override void LoadBackup(BotBackup backup)
{
var data = JsonConvert.DeserializeObject<TradingBotBackup>(backup.Data);
Config = new TradingBotConfig
{
AccountName = data.AccountName,
MoneyManagement = data.MoneyManagement,
Ticker = data.Ticker,
ScenarioName = data.ScenarioName,
Timeframe = data.Timeframe,
IsForBacktest = false, // Always false when loading from backup
IsForWatchingOnly = data.IsForWatchingOnly,
BotTradingBalance = data.BotTradingBalance,
BotType = data.BotType,
CooldownPeriod = data.CooldownPeriod,
MaxLossStreak = data.MaxLossStreak,
MaxPositionTimeHours = data.MaxPositionTimeHours == 0m ? null : data.MaxPositionTimeHours,
FlipOnlyWhenInProfit = data.FlipOnlyWhenInProfit,
CloseEarlyWhenProfitable = data.CloseEarlyWhenProfitable,
Name = data.Name
};
Signals = data.Signals;
Positions = data.Positions;
WalletBalances = data.WalletBalances;
// Load the configuration directly
Config = data.Config;
// Ensure IsForBacktest is always false when loading from backup
Config.IsForBacktest = false;
// Load runtime state
Signals = data.Signals ?? new HashSet<Signal>();
Positions = data.Positions ?? new List<Position>();
WalletBalances = data.WalletBalances ?? new Dictionary<DateTime, decimal>();
PreloadSince = data.StartupTime;
Identifier = backup.Identifier;
User = backup.User;
@@ -1307,7 +1362,7 @@ public class TradingBot : Bot, ITradingBot
// Create a fake signal for manual position opening
var signal = new Signal(Config.Ticker, direction, Confidence.Low, lastCandle, lastCandle.Date,
TradingExchanges.GmxV2,
IndicatorType.Stc, SignalType.Signal);
IndicatorType.Stc, SignalType.Signal, "Manual Signal");
signal.Status = SignalStatus.WaitingForPosition; // Ensure status is correct
signal.User = Account.User; // Assign user
@@ -1433,7 +1488,7 @@ public class TradingBot : Bot, ITradingBot
}
// If scenario changed, reload it
var currentScenario = Scenario?.Name;
var currentScenario = Config.Scenario?.Name;
if (Config.ScenarioName != currentScenario)
{
LoadScenario(Config.ScenarioName);
@@ -1485,29 +1540,36 @@ public class TradingBot : Bot, ITradingBot
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
FlipPosition = Config.FlipPosition,
Name = Config.Name,
CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable
CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable,
UseSynthApi = Config.UseSynthApi,
};
}
}
public class TradingBotBackup
{
public string Name { get; set; }
public BotType BotType { get; set; }
/// <summary>
/// The complete trading bot configuration
/// </summary>
public TradingBotConfig Config { get; set; }
/// <summary>
/// Runtime state: Active signals for the bot
/// </summary>
public HashSet<Signal> Signals { get; set; }
/// <summary>
/// Runtime state: Open and closed positions for the bot
/// </summary>
public List<Position> Positions { get; set; }
public Timeframe Timeframe { get; set; }
public Ticker Ticker { get; set; }
public string ScenarioName { get; set; }
public string AccountName { get; set; }
public bool IsForWatchingOnly { get; set; }
/// <summary>
/// Runtime state: Historical wallet balances over time
/// </summary>
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
public MoneyManagement MoneyManagement { get; set; }
/// <summary>
/// Runtime state: When the bot was started
/// </summary>
public DateTime StartupTime { get; set; }
public decimal BotTradingBalance { get; set; }
public int CooldownPeriod { get; set; }
public int MaxLossStreak { get; set; }
public decimal MaxPositionTimeHours { get; set; }
public bool FlipOnlyWhenInProfit { get; set; }
public bool CloseEarlyWhenProfitable { get; set; }
}

View File

@@ -45,7 +45,7 @@ namespace Managing.Application.ManageBot
return _botRepository.GetBots().FirstOrDefault(b => b.Identifier == identifier);
}
public void SaveOrUpdateBotBackup(User user, string identifier, BotType botType, BotStatus status, string data)
public void SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, string data)
{
var backup = GetBotBackup(identifier);
@@ -62,7 +62,6 @@ namespace Managing.Application.ManageBot
LastStatus = status,
User = user,
Identifier = identifier,
BotType = botType,
Data = data
};
@@ -118,40 +117,29 @@ namespace Managing.Application.ManageBot
object bot = null;
Task botTask = null;
switch (backupBot.BotType)
var scalpingBotData = JsonConvert.DeserializeObject<TradingBotBackup>(backupBot.Data);
// Get the config directly from the backup
var scalpingConfig = scalpingBotData.Config;
// Ensure the money management is properly loaded from database if needed
if (scalpingConfig.MoneyManagement != null &&
!string.IsNullOrEmpty(scalpingConfig.MoneyManagement.Name))
{
case BotType.ScalpingBot:
case BotType.FlippingBot:
var scalpingBotData = JsonConvert.DeserializeObject<TradingBotBackup>(backupBot.Data);
var scalpingMoneyManagement =
_moneyManagementService.GetMoneyMangement(scalpingBotData.MoneyManagement.Name).Result;
// Create config from backup data
var scalpingConfig = new TradingBotConfig
{
AccountName = scalpingBotData.AccountName,
MoneyManagement = scalpingMoneyManagement,
Ticker = scalpingBotData.Ticker,
ScenarioName = scalpingBotData.ScenarioName,
Timeframe = scalpingBotData.Timeframe,
IsForWatchingOnly = scalpingBotData.IsForWatchingOnly,
BotTradingBalance = scalpingBotData.BotTradingBalance,
BotType = scalpingBotData.BotType,
Name = scalpingBotData.Name,
CooldownPeriod = scalpingBotData.CooldownPeriod,
MaxLossStreak = scalpingBotData.MaxLossStreak,
MaxPositionTimeHours = scalpingBotData.MaxPositionTimeHours == 0m ? null : scalpingBotData.MaxPositionTimeHours,
FlipOnlyWhenInProfit = scalpingBotData.FlipOnlyWhenInProfit,
IsForBacktest = false,
FlipPosition = false,
CloseEarlyWhenProfitable = scalpingBotData.CloseEarlyWhenProfitable
};
bot = CreateTradingBot(scalpingConfig);
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
break;
var moneyManagement = _moneyManagementService
.GetMoneyMangement(scalpingConfig.MoneyManagement.Name).Result;
if (moneyManagement != null)
{
scalpingConfig.MoneyManagement = moneyManagement;
}
}
// Ensure critical properties are set correctly for restored bots
scalpingConfig.IsForBacktest = false;
bot = CreateTradingBot(scalpingConfig);
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
if (bot != null && botTask != null)
{
var botWrapper = new BotTaskWrapper(botTask, bot.GetType(), bot);
@@ -258,14 +246,14 @@ namespace Managing.Application.ManageBot
// Update the bot configuration first
var updateResult = await tradingBot.UpdateConfiguration(newConfig, allowNameChange: true);
if (updateResult)
{
// Update the dictionary key
if (_botTasks.TryRemove(identifier, out var removedWrapper))
{
_botTasks.TryAdd(newConfig.Name, removedWrapper);
// Update the backup with the new identifier
if (!newConfig.IsForBacktest)
{
@@ -275,7 +263,7 @@ namespace Managing.Application.ManageBot
}
}
}
return updateResult;
}
else
@@ -288,7 +276,6 @@ namespace Managing.Application.ManageBot
return false;
}
public ITradingBot CreateTradingBot(TradingBotConfig config)
{

View File

@@ -52,7 +52,9 @@ namespace Managing.Application.ManageBot
var usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString());
if (usdcBalance == null || usdcBalance.Value < request.Config.BotTradingBalance)
if (usdcBalance == null ||
usdcBalance.Value < Constants.GMX.Config.MinimumPositionAmount ||
usdcBalance.Value < request.Config.BotTradingBalance)
{
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
}
@@ -64,12 +66,14 @@ namespace Managing.Application.ManageBot
MoneyManagement = request.Config.MoneyManagement,
Ticker = request.Config.Ticker,
ScenarioName = request.Config.ScenarioName,
Scenario = request.Config.Scenario,
Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly,
BotTradingBalance = request.Config.BotTradingBalance,
BotType = request.Config.BotType,
IsForBacktest = request.Config.IsForBacktest,
CooldownPeriod = request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set
CooldownPeriod =
request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set
MaxLossStreak = request.Config.MaxLossStreak,
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, // Properly handle nullable value
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
@@ -85,15 +89,15 @@ namespace Managing.Application.ManageBot
bot.User = request.User;
_botService.AddSimpleBotToCache(bot);
return bot.GetStatus();
case BotType.ScalpingBot:
case BotType.FlippingBot:
var tradingBot = _botFactory.CreateTradingBot(configToUse);
tradingBot.User = request.User;
// Log the configuration being used
await LogBotConfigurationAsync(tradingBot, $"{configToUse.BotType} created");
_botService.AddTradingBotToCache(tradingBot);
return tradingBot.GetStatus();
}
@@ -112,16 +116,16 @@ namespace Managing.Application.ManageBot
{
var config = bot.GetConfiguration();
var logMessage = $"{context} - Bot: {config.Name}, " +
$"Type: {config.BotType}, " +
$"Account: {config.AccountName}, " +
$"Ticker: {config.Ticker}, " +
$"Balance: {config.BotTradingBalance}, " +
$"MaxTime: {config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " +
$"FlipOnlyProfit: {config.FlipOnlyWhenInProfit}, " +
$"FlipPosition: {config.FlipPosition}, " +
$"Cooldown: {config.CooldownPeriod}, " +
$"MaxLoss: {config.MaxLossStreak}";
$"Type: {config.BotType}, " +
$"Account: {config.AccountName}, " +
$"Ticker: {config.Ticker}, " +
$"Balance: {config.BotTradingBalance}, " +
$"MaxTime: {config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " +
$"FlipOnlyProfit: {config.FlipOnlyWhenInProfit}, " +
$"FlipPosition: {config.FlipPosition}, " +
$"Cooldown: {config.CooldownPeriod}, " +
$"MaxLoss: {config.MaxLossStreak}";
// Log through the bot's logger (this will use the bot's logging mechanism)
// For now, we'll just add a comment that this could be enhanced with actual logging
// Console.WriteLine(logMessage); // Could be replaced with proper logging

View File

@@ -79,7 +79,7 @@ namespace Managing.Application.Scenarios
return _tradingService.GetScenarioByName(name);
}
public IEnumerable<Indicator> GetStrategies()
public IEnumerable<Indicator> GetIndicators()
{
return _tradingService.GetStrategies();
}

View File

@@ -0,0 +1,324 @@
using System.Text.Json;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Synth.Models;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Synth;
/// <summary>
/// Client for communicating with the Synth API
/// </summary>
public class SynthApiClient : ISynthApiClient, IDisposable
{
private readonly HttpClient _httpClient;
private readonly ILogger<SynthApiClient> _logger;
private readonly JsonSerializerOptions _jsonOptions;
// Private configuration - should come from app settings or environment variables
private readonly string _apiKey;
private readonly string _baseUrl;
public SynthApiClient(HttpClient httpClient, ILogger<SynthApiClient> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// TODO: These should come from IConfiguration or environment variables
_apiKey = Environment.GetEnvironmentVariable("SYNTH_API_KEY") ??
"bfd2a078b412452af2e01ca74b2a7045d4ae411a85943342";
_baseUrl = Environment.GetEnvironmentVariable("SYNTH_BASE_URL") ?? "https://api.synthdata.co";
// Configure HttpClient once
ConfigureHttpClient();
// Configure JSON options
_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
/// <summary>
/// Configures the HTTP client with API settings
/// </summary>
private void ConfigureHttpClient()
{
// Validate API configuration
if (string.IsNullOrEmpty(_apiKey) || string.IsNullOrEmpty(_baseUrl))
{
throw new InvalidOperationException(
"Synth API configuration is missing. Please set SYNTH_API_KEY and SYNTH_BASE_URL environment variables.");
}
// Set base address and authorization
_httpClient.BaseAddress = new Uri(_baseUrl);
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Apikey {_apiKey}");
_httpClient.Timeout = TimeSpan.FromSeconds(30);
}
/// <summary>
/// Fetches the current leaderboard from Synth API
/// </summary>
public async Task<List<MinerInfo>> GetLeaderboardAsync(SynthConfiguration config)
{
try
{
_logger.LogInformation("🔍 **Synth API** - Fetching leaderboard");
var response = await _httpClient.GetAsync("/leaderboard/latest");
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
$"Synth API leaderboard request failed: {response.StatusCode} - {response.ReasonPhrase}");
return new List<MinerInfo>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var miners = JsonSerializer.Deserialize<List<MinerInfo>>(jsonContent, _jsonOptions);
_logger.LogInformation($"📊 **Synth API** - Retrieved {miners?.Count ?? 0} miners from leaderboard");
return miners ?? new List<MinerInfo>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error while fetching Synth leaderboard");
return new List<MinerInfo>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, "Timeout while fetching Synth leaderboard");
return new List<MinerInfo>();
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON deserialization error while parsing Synth leaderboard");
return new List<MinerInfo>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while fetching Synth leaderboard");
return new List<MinerInfo>();
}
}
/// <summary>
/// Fetches historical leaderboard data from Synth API for a specific time range
/// </summary>
public async Task<List<MinerInfo>> GetHistoricalLeaderboardAsync(DateTime startTime, DateTime endTime, SynthConfiguration config)
{
try
{
// Format dates to ISO 8601 format as required by the API
var startTimeStr = Uri.EscapeDataString(startTime.ToString("yyyy-MM-ddTHH:mm:ssZ"));
var endTimeStr = Uri.EscapeDataString(endTime.ToString("yyyy-MM-ddTHH:mm:ssZ"));
var url = $"/leaderboard/historical?start_time={startTimeStr}&end_time={endTimeStr}";
_logger.LogInformation($"🔍 **Synth API** - Fetching historical leaderboard from {startTime:yyyy-MM-dd HH:mm} to {endTime:yyyy-MM-dd HH:mm}");
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
$"Synth API historical leaderboard request failed: {response.StatusCode} - {response.ReasonPhrase}");
return new List<MinerInfo>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var miners = JsonSerializer.Deserialize<List<MinerInfo>>(jsonContent, _jsonOptions);
_logger.LogInformation($"📊 **Synth API** - Retrieved {miners?.Count ?? 0} miners from historical leaderboard");
return miners ?? new List<MinerInfo>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error while fetching Synth historical leaderboard");
return new List<MinerInfo>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, "Timeout while fetching Synth historical leaderboard");
return new List<MinerInfo>();
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON deserialization error while parsing Synth historical leaderboard");
return new List<MinerInfo>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while fetching Synth historical leaderboard");
return new List<MinerInfo>();
}
}
/// <summary>
/// Fetches latest predictions from specified miners
/// </summary>
public async Task<List<MinerPrediction>> GetMinerPredictionsAsync(
List<int> minerUids,
string asset,
int timeIncrement,
int timeLength,
SynthConfiguration config)
{
if (minerUids == null || !minerUids.Any())
{
_logger.LogWarning("No miner UIDs provided for prediction request");
return new List<MinerPrediction>();
}
try
{
// Build URL with proper array formatting for miner parameter
var queryParams = new List<string>
{
$"asset={Uri.EscapeDataString(asset)}",
$"time_increment={timeIncrement}",
$"time_length={timeLength}"
};
// Add each miner UID as a separate parameter (standard array query parameter format)
foreach (var minerUid in minerUids)
{
queryParams.Add($"miner={minerUid}");
}
var url = $"/prediction/latest?{string.Join("&", queryParams)}";
_logger.LogInformation(
$"🔮 **Synth API** - Fetching predictions for {minerUids.Count} miners, asset: {asset}, time: {timeLength}s");
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
$"Synth API predictions request failed: {response.StatusCode} - {response.ReasonPhrase}");
return new List<MinerPrediction>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var predictions = JsonSerializer.Deserialize<List<MinerPrediction>>(jsonContent, _jsonOptions);
var totalPaths = predictions?.Sum(p => p.NumSimulations) ?? 0;
_logger.LogInformation(
$"📈 **Synth API** - Retrieved {predictions?.Count ?? 0} predictions with {totalPaths} total simulation paths");
return predictions ?? new List<MinerPrediction>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, $"HTTP error while fetching Synth predictions for {asset}");
return new List<MinerPrediction>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, $"Timeout while fetching Synth predictions for {asset}");
return new List<MinerPrediction>();
}
catch (JsonException ex)
{
_logger.LogError(ex, $"JSON deserialization error while parsing Synth predictions for {asset}");
return new List<MinerPrediction>();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error while fetching Synth predictions for {asset}");
return new List<MinerPrediction>();
}
}
/// <summary>
/// Fetches historical predictions from specified miners for a specific time point
/// </summary>
public async Task<List<MinerPrediction>> GetHistoricalMinerPredictionsAsync(
List<int> minerUids,
string asset,
DateTime startTime,
int timeIncrement,
int timeLength,
SynthConfiguration config)
{
if (minerUids == null || !minerUids.Any())
{
_logger.LogWarning("No miner UIDs provided for historical prediction request");
return new List<MinerPrediction>();
}
try
{
// Format start time to ISO 8601 format as required by the API
var startTimeStr = Uri.EscapeDataString(startTime.ToString("yyyy-MM-ddTHH:mm:ssZ"));
// Build URL with proper array formatting for miner parameter
var queryParams = new List<string>
{
$"asset={Uri.EscapeDataString(asset)}",
$"start_time={startTimeStr}",
$"time_increment={timeIncrement}",
$"time_length={timeLength}"
};
// Add each miner UID as a separate parameter (standard array query parameter format)
foreach (var minerUid in minerUids)
{
queryParams.Add($"miner={minerUid}");
}
var url = $"/prediction/historical?{string.Join("&", queryParams)}";
_logger.LogInformation(
$"🔮 **Synth API** - Fetching historical predictions for {minerUids.Count} miners, asset: {asset}, time: {startTime:yyyy-MM-dd HH:mm}, duration: {timeLength}s");
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
$"Synth API historical predictions request failed: {response.StatusCode} - {response.ReasonPhrase}");
return new List<MinerPrediction>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var predictions = JsonSerializer.Deserialize<List<MinerPrediction>>(jsonContent, _jsonOptions);
var totalPaths = predictions?.Sum(p => p.NumSimulations) ?? 0;
_logger.LogInformation(
$"📈 **Synth API** - Retrieved {predictions?.Count ?? 0} historical predictions with {totalPaths} total simulation paths");
return predictions ?? new List<MinerPrediction>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, $"HTTP error while fetching Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, $"Timeout while fetching Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
catch (JsonException ex)
{
_logger.LogError(ex, $"JSON deserialization error while parsing Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error while fetching Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
}
public void Dispose()
{
_httpClient?.Dispose();
}
}

View File

@@ -0,0 +1,169 @@
using Managing.Domain.Synth.Models;
namespace Managing.Application.Synth;
/// <summary>
/// Helper class for creating and configuring Synth API integration
/// </summary>
public static class SynthConfigurationHelper
{
/// <summary>
/// Creates a default Synth configuration for live trading
/// </summary>
/// <returns>A configured SynthConfiguration instance</returns>
public static SynthConfiguration CreateLiveTradingConfig()
{
return new SynthConfiguration
{
IsEnabled = true,
TopMinersCount = 10,
TimeIncrement = 300, // 5 minutes
DefaultTimeLength = 86400, // 24 hours
MaxLiquidationProbability = 0.10m, // 10% max risk
PredictionCacheDurationMinutes = 5,
UseForPositionSizing = true,
UseForSignalFiltering = true,
UseForDynamicStopLoss = true
};
}
/// <summary>
/// Creates a conservative Synth configuration with lower risk tolerances
/// </summary>
/// <returns>A conservative SynthConfiguration instance</returns>
public static SynthConfiguration CreateConservativeConfig()
{
return new SynthConfiguration
{
IsEnabled = true,
TopMinersCount = 10,
TimeIncrement = 300, // 5 minutes
DefaultTimeLength = 86400, // 24 hours
MaxLiquidationProbability = 0.05m, // 5% max risk (more conservative)
PredictionCacheDurationMinutes = 3, // More frequent updates
UseForPositionSizing = true,
UseForSignalFiltering = true,
UseForDynamicStopLoss = true
};
}
/// <summary>
/// Creates an aggressive Synth configuration with higher risk tolerances
/// </summary>
/// <returns>An aggressive SynthConfiguration instance</returns>
public static SynthConfiguration CreateAggressiveConfig()
{
return new SynthConfiguration
{
IsEnabled = true,
TopMinersCount = 15, // More miners for broader consensus
TimeIncrement = 300, // 5 minutes
DefaultTimeLength = 86400, // 24 hours
MaxLiquidationProbability = 0.15m, // 15% max risk (more aggressive)
PredictionCacheDurationMinutes = 7, // Less frequent updates to reduce API calls
UseForPositionSizing = true,
UseForSignalFiltering = false, // Don't filter signals in aggressive mode
UseForDynamicStopLoss = true
};
}
/// <summary>
/// Creates a disabled Synth configuration (bot will operate without Synth predictions)
/// </summary>
/// <returns>A disabled SynthConfiguration instance</returns>
public static SynthConfiguration CreateDisabledConfig()
{
return new SynthConfiguration
{
IsEnabled = false,
TopMinersCount = 10,
TimeIncrement = 300,
DefaultTimeLength = 86400,
MaxLiquidationProbability = 0.10m,
PredictionCacheDurationMinutes = 5,
UseForPositionSizing = false,
UseForSignalFiltering = false,
UseForDynamicStopLoss = false
};
}
/// <summary>
/// Creates a Synth configuration optimized for backtesting (disabled)
/// </summary>
/// <returns>A backtesting-optimized SynthConfiguration instance</returns>
public static SynthConfiguration CreateBacktestConfig()
{
// Synth predictions are not available for historical data, so always disabled for backtests
return CreateDisabledConfig();
}
/// <summary>
/// Validates and provides suggestions for improving a Synth configuration
/// </summary>
/// <param name="config">The configuration to validate</param>
/// <returns>List of validation messages and suggestions</returns>
public static List<string> ValidateConfiguration(SynthConfiguration config)
{
var messages = new List<string>();
if (config == null)
{
messages.Add("❌ Configuration is null");
return messages;
}
if (!config.IsEnabled)
{
messages.Add(" Synth API is disabled - bot will operate without predictions");
return messages;
}
if (config.TopMinersCount <= 0)
{
messages.Add("❌ TopMinersCount must be greater than 0");
}
else if (config.TopMinersCount > 20)
{
messages.Add("⚠️ TopMinersCount > 20 may result in slower performance and higher API usage");
}
if (config.TimeIncrement <= 0)
{
messages.Add("❌ TimeIncrement must be greater than 0");
}
if (config.DefaultTimeLength <= 0)
{
messages.Add("❌ DefaultTimeLength must be greater than 0");
}
if (config.MaxLiquidationProbability < 0 || config.MaxLiquidationProbability > 1)
{
messages.Add("❌ MaxLiquidationProbability must be between 0 and 1");
}
else if (config.MaxLiquidationProbability < 0.02m)
{
messages.Add("⚠️ MaxLiquidationProbability < 2% is very conservative and may block many trades");
}
else if (config.MaxLiquidationProbability > 0.20m)
{
messages.Add("⚠️ MaxLiquidationProbability > 20% is very aggressive and may increase risk");
}
if (config.PredictionCacheDurationMinutes <= 0)
{
messages.Add("❌ PredictionCacheDurationMinutes must be greater than 0");
}
else if (config.PredictionCacheDurationMinutes < 1)
{
messages.Add("⚠️ Cache duration < 1 minute may result in excessive API calls");
}
if (messages.Count == 0)
{
messages.Add("✅ Configuration appears valid");
}
return messages;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,12 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Synth.Models;
using Managing.Domain.Trades;
using Managing.Infrastructure.Evm.Models.Privy;
using Microsoft.Extensions.Logging;
@@ -22,6 +24,7 @@ public class TradingService : ITradingService
private readonly IStatisticRepository _statisticRepository;
private readonly IEvmManager _evmManager;
private readonly ILogger<TradingService> _logger;
private readonly ISynthPredictionService _synthPredictionService;
public TradingService(
ITradingRepository tradingRepository,
@@ -31,7 +34,8 @@ public class TradingService : ITradingService
ICacheService cacheService,
IMessengerService messengerService,
IStatisticRepository statisticRepository,
IEvmManager evmManager)
IEvmManager evmManager,
ISynthPredictionService synthPredictionService)
{
_tradingRepository = tradingRepository;
_exchangeService = exchangeService;
@@ -41,6 +45,7 @@ public class TradingService : ITradingService
_messengerService = messengerService;
_statisticRepository = statisticRepository;
_evmManager = evmManager;
_synthPredictionService = synthPredictionService;
}
public void DeleteScenario(string name)
@@ -397,4 +402,25 @@ public class TradingService : ITradingService
return new PrivyInitAddressResponse { Success = false, Error = ex.Message };
}
}
// Synth API integration methods
public async Task<SignalValidationResult> ValidateSynthSignalAsync(Signal signal, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest)
{
return await _synthPredictionService.ValidateSignalAsync(signal, currentPrice, botConfig, isBacktest);
}
public async Task<bool> AssessSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest)
{
return await _synthPredictionService.AssessPositionRiskAsync(ticker, direction, currentPrice,
botConfig, isBacktest);
}
public async Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction,
decimal currentPrice, decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig)
{
return await _synthPredictionService.MonitorPositionRiskAsync(ticker, direction, currentPrice, liquidationPrice,
positionIdentifier, botConfig);
}
}

View File

@@ -14,6 +14,7 @@ using Managing.Application.MoneyManagements;
using Managing.Application.Scenarios;
using Managing.Application.Shared;
using Managing.Application.Shared.Behaviours;
using Managing.Application.Synth;
using Managing.Application.Trading;
using Managing.Application.Trading.Commands;
using Managing.Application.Users;
@@ -95,6 +96,8 @@ public static class ApiBootstrap
services.AddTransient<IPrivyService, PrivyService>();
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
services.AddTransient<IWebhookService, WebhookService>();
services.AddTransient<ISynthPredictionService, SynthPredictionService>();
services.AddTransient<ISynthApiClient, SynthApiClient>();
return services;
}
@@ -133,6 +136,7 @@ public static class ApiBootstrap
services.AddTransient<IBotRepository, BotRepository>();
services.AddTransient<IWorkerRepository, WorkerRepository>();
services.AddTransient<IAgentBalanceRepository, AgentBalanceRepository>();
services.AddTransient<ISynthRepository, SynthRepository>();
// Cache
services.AddDistributedMemoryCache();

View File

@@ -12,6 +12,7 @@ using Managing.Application.ManageBot;
using Managing.Application.MoneyManagements;
using Managing.Application.Scenarios;
using Managing.Application.Shared;
using Managing.Application.Synth;
using Managing.Application.Trading;
using Managing.Application.Trading.Commands;
using Managing.Application.Users;
@@ -66,6 +67,7 @@ public static class WorkersBootstrap
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IBacktester, Backtester>();
services.AddSingleton<IBotService, BotService>();
services.AddSingleton<ISynthPredictionService, SynthPredictionService>();
services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>();
services.AddTransient<ICommandHandler<ClosePositionCommand, Position>, ClosePositionCommandHandler>();
@@ -111,6 +113,7 @@ public static class WorkersBootstrap
services.AddTransient<IBacktestRepository, BacktestRepository>();
services.AddTransient<IBotRepository, BotRepository>();
services.AddTransient<IUserRepository, UserRepository>();
services.AddTransient<ISynthRepository, SynthRepository>();
// Cache
services.AddDistributedMemoryCache();
@@ -126,6 +129,7 @@ public static class WorkersBootstrap
services.AddSingleton<IBinanceSocketClient, BinanceSocketClient>();
services.AddSingleton<IKrakenSocketClient, KrakenSocketClient>();
services.AddSingleton<IPrivyService, PrivyService>();
services.AddSingleton<ISynthApiClient, SynthApiClient>();
// Web3Proxy Configuration
services.Configure<Web3ProxySettings>(configuration.GetSection("Web3Proxy"));

View File

@@ -405,4 +405,14 @@ public static class Enums
Position,
MoneyManagement
}
/// <summary>
/// Risk tolerance levels for trading strategies
/// </summary>
public enum RiskToleranceLevel
{
Conservative = 1,
Moderate = 2,
Aggressive = 3
}
}

View File

@@ -1,10 +1,26 @@
using System.Text.Json.Serialization;
namespace Managing.Core.FixedSizedQueue;
public class FixedSizeQueue<T> : Queue<T>
{
private readonly int _maxSize;
/// <summary>
/// Parameterless constructor for serialization support
/// </summary>
public FixedSizeQueue() : this(500) // Default size
{
}
[JsonConstructor]
public FixedSizeQueue(int maxSize) => _maxSize = maxSize;
/// <summary>
/// Gets the maximum size of the queue (for serialization)
/// </summary>
public int MaxSize => _maxSize;
public new void Enqueue(T item)
{
while (Count >= _maxSize) Dequeue();

View File

@@ -24,7 +24,7 @@ public class Backtest
Signals = signals;
Candles = candles;
WalletBalances = new List<KeyValuePair<DateTime, decimal>>();
StrategiesValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
IndicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
// Initialize start and end dates if candles are provided
if (candles != null && candles.Count > 0)
@@ -55,7 +55,7 @@ public class Backtest
[Required] public List<KeyValuePair<DateTime, decimal>> WalletBalances { get; set; }
[Required] public MoneyManagement OptimizedMoneyManagement { get; set; }
[Required] public User User { get; set; }
[Required] public Dictionary<IndicatorType, IndicatorsResultBase> StrategiesValues { get; set; }
[Required] public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; }
[Required] public double Score { get; set; }
/// <summary>

View File

@@ -5,7 +5,6 @@ namespace Managing.Domain.Bots;
public class BotBackup
{
public BotType BotType { get; set; }
public string Identifier { get; set; }
public User User { get; set; }
public string Data { get; set; }

View File

@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Risk;
using Managing.Domain.Scenarios;
using static Managing.Common.Enums;
@@ -20,6 +21,13 @@ public class TradingBotConfig
[Required] public bool FlipPosition { get; set; }
[Required] public string Name { get; set; }
/// <summary>
/// Risk management configuration for advanced probabilistic analysis and position sizing.
/// Contains all configurable parameters for Expected Utility Theory, Kelly Criterion, and probability thresholds.
/// If null, default risk management settings will be used.
/// </summary>
public RiskManagement RiskManagement { get; set; } = new();
/// <summary>
/// The 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.
@@ -52,4 +60,27 @@ public class TradingBotConfig
/// </summary>
[Required]
public bool FlipOnlyWhenInProfit { get; set; } = true;
/// <summary>
/// Whether to use Synth API for probabilistic price forecasts and risk assessment.
/// When true, the bot will use Synth predictions for signal filtering, position risk assessment, and position monitoring.
/// When false, the bot operates in traditional mode without Synth predictions.
/// The actual Synth configuration is managed centrally in SynthPredictionService.
/// </summary>
public bool UseSynthApi { get; set; } = false;
/// <summary>
/// Whether to use Synth predictions for position sizing adjustments and risk assessment
/// </summary>
public bool UseForPositionSizing { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for signal filtering
/// </summary>
public bool UseForSignalFiltering { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary>
public bool UseForDynamicStopLoss { get; set; } = true;
}

View File

@@ -0,0 +1,192 @@
using System.ComponentModel.DataAnnotations;
using Managing.Common;
namespace Managing.Domain.Risk;
/// <summary>
/// Risk management configuration for trading bots
/// Contains all configurable risk parameters for probabilistic analysis and position sizing
/// </summary>
public class RiskManagement
{
/// <summary>
/// Threshold for adverse probability in signal validation (default: 20%)
/// Signals with SL probability above this threshold may be filtered out
/// Range: 0.05 (5%) to 0.50 (50%)
/// </summary>
[Range(0.05, 0.50)]
[Required]
public decimal AdverseProbabilityThreshold { get; set; } = 0.20m;
/// <summary>
/// Threshold for favorable probability in signal validation (default: 30%)
/// Used for additional signal filtering and confidence assessment
/// Range: 0.10 (10%) to 0.70 (70%)
/// </summary>
[Range(0.10, 0.70)]
[Required]
public decimal FavorableProbabilityThreshold { get; set; } = 0.30m;
/// <summary>
/// Risk aversion parameter for Expected Utility calculations (default: 1.0)
/// Higher values = more risk-averse behavior in utility calculations
/// Range: 0.1 (risk-seeking) to 5.0 (highly risk-averse)
/// </summary>
[Range(0.1, 5.0)]
[Required]
public decimal RiskAversion { get; set; } = 1.0m;
/// <summary>
/// Minimum Kelly Criterion fraction to consider a trade favorable (default: 1%)
/// Trades with Kelly fraction below this threshold are considered unfavorable
/// Range: 0.5% to 10%
/// </summary>
[Range(0.005, 0.10)]
[Required]
public decimal KellyMinimumThreshold { get; set; } = 0.01m;
/// <summary>
/// Maximum Kelly Criterion fraction cap for practical risk management (default: 25%)
/// Prevents over-allocation even when Kelly suggests higher percentages
/// Range: 5% to 50%
/// </summary>
[Range(0.05, 0.50)]
[Required]
public decimal KellyMaximumCap { get; set; } = 0.25m;
/// <summary>
/// Maximum acceptable liquidation probability for position risk assessment (default: 10%)
/// Positions with higher liquidation risk may be blocked or reduced
/// Range: 5% to 30%
/// </summary>
[Range(0.05, 0.30)]
[Required]
public decimal MaxLiquidationProbability { get; set; } = 0.10m;
/// <summary>
/// Time horizon in hours for signal validation analysis (default: 24 hours)
/// Longer horizons provide more stable predictions but less responsive signals
/// Range: 1 hour to 168 hours (1 week)
/// </summary>
[Range(1, 168)]
[Required]
public int SignalValidationTimeHorizonHours { get; set; } = 24;
/// <summary>
/// Time horizon in hours for position risk monitoring (default: 6 hours)
/// Shorter horizons for more frequent risk updates on open positions
/// Range: 1 hour to 48 hours
/// </summary>
[Range(1, 48)]
[Required]
public int PositionMonitoringTimeHorizonHours { get; set; } = 6;
/// <summary>
/// Probability threshold for issuing position risk warnings (default: 20%)
/// Positions exceeding this liquidation risk will trigger warnings
/// Range: 10% to 40%
/// </summary>
[Range(0.10, 0.40)]
[Required]
public decimal PositionWarningThreshold { get; set; } = 0.20m;
/// <summary>
/// Probability threshold for automatic position closure (default: 50%)
/// Positions exceeding this liquidation risk will be automatically closed
/// Range: 30% to 80%
/// </summary>
[Range(0.30, 0.80)]
[Required]
public decimal PositionAutoCloseThreshold { get; set; } = 0.50m;
/// <summary>
/// Fractional Kelly multiplier for conservative position sizing (default: 1.0)
/// Values less than 1.0 implement fractional Kelly (e.g., 0.5 = half-Kelly)
/// Range: 0.1 to 1.0
/// </summary>
[Range(0.1, 1.0)]
[Required]
public decimal KellyFractionalMultiplier { get; set; } = 1.0m;
/// <summary>
/// Risk tolerance level affecting overall risk calculations
/// </summary>
[Required]
public Enums.RiskToleranceLevel RiskTolerance { get; set; } = Enums.RiskToleranceLevel.Moderate;
/// <summary>
/// Whether to use Expected Utility Theory for decision making
/// </summary>
[Required]
public bool UseExpectedUtility { get; set; } = true;
/// <summary>
/// Whether to use Kelly Criterion for position sizing recommendations
/// </summary>
[Required]
public bool UseKellyCriterion { get; set; } = true;
/// <summary>
/// Validates that the risk management configuration is coherent
/// </summary>
/// <returns>True if configuration is valid, false otherwise</returns>
public bool IsConfigurationValid()
{
// Ensure favorable threshold is higher than adverse threshold
if (FavorableProbabilityThreshold <= AdverseProbabilityThreshold)
return false;
// Ensure Kelly minimum is less than maximum
if (KellyMinimumThreshold >= KellyMaximumCap)
return false;
// Ensure warning threshold is less than auto-close threshold
if (PositionWarningThreshold >= PositionAutoCloseThreshold)
return false;
// Ensure signal validation horizon is longer than position monitoring
if (SignalValidationTimeHorizonHours < PositionMonitoringTimeHorizonHours)
return false;
return true;
}
/// <summary>
/// Gets a preset configuration based on risk tolerance level
/// </summary>
/// <param name="tolerance">Risk tolerance level</param>
/// <returns>Configured RiskManagement instance</returns>
public static RiskManagement GetPresetConfiguration(Enums.RiskToleranceLevel tolerance)
{
return tolerance switch
{
Enums.RiskToleranceLevel.Conservative => new RiskManagement
{
AdverseProbabilityThreshold = 0.15m,
FavorableProbabilityThreshold = 0.40m,
RiskAversion = 2.0m,
KellyMinimumThreshold = 0.02m,
KellyMaximumCap = 0.15m,
MaxLiquidationProbability = 0.08m,
PositionWarningThreshold = 0.15m,
PositionAutoCloseThreshold = 0.35m,
KellyFractionalMultiplier = 0.5m,
RiskTolerance = tolerance
},
Enums.RiskToleranceLevel.Aggressive => new RiskManagement
{
AdverseProbabilityThreshold = 0.30m,
FavorableProbabilityThreshold = 0.25m,
RiskAversion = 0.5m,
KellyMinimumThreshold = 0.005m,
KellyMaximumCap = 0.40m,
MaxLiquidationProbability = 0.15m,
PositionWarningThreshold = 0.30m,
PositionAutoCloseThreshold = 0.70m,
KellyFractionalMultiplier = 1.0m,
RiskTolerance = tolerance
},
_ => new RiskManagement { RiskTolerance = tolerance } // Moderate (default values)
};
}
}

View File

@@ -108,15 +108,21 @@ public static class TradingBox
}
}
// Keep only the latest signal per indicator to avoid count mismatch
var latestSignalsPerIndicator = signalOnCandles
.GroupBy(s => s.IndicatorName)
.Select(g => g.OrderByDescending(s => s.Date).First())
.ToHashSet();
// Remove the restrictive requirement that ALL strategies must produce signals
// Instead, let ComputeSignals handle the logic based on what we have
if (!signalOnCandles.Any())
if (!latestSignalsPerIndicator.Any())
{
return null; // No signals from any strategy
}
var data = newCandles.First();
return ComputeSignals(strategies, signalOnCandles, MiscExtensions.ParseEnum<Ticker>(data.Ticker),
return ComputeSignals(strategies, latestSignalsPerIndicator, MiscExtensions.ParseEnum<Ticker>(data.Ticker),
data.Timeframe, config);
}
@@ -136,51 +142,88 @@ public static class TradingBox
}
// Check if all strategies produced signals - this is required for composite signals
if (signalOnCandles.Count != strategies.Count)
var strategyNames = strategies.Select(s => s.Name).ToHashSet();
var signalIndicatorNames = signalOnCandles.Select(s => s.IndicatorName).ToHashSet();
if (!strategyNames.SetEquals(signalIndicatorNames))
{
// Not all strategies produced signals - composite signal requires all strategies to contribute
return null;
}
// Group signals by type for analysis
var signalStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Signal).ToList();
var trendStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Trend).ToList();
var contextStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Context).ToList();
var signals = signalOnCandles.Where(s => s.SignalType == SignalType.Signal).ToList();
var trendSignals = signalOnCandles.Where(s => s.SignalType == SignalType.Trend).ToList();
var contextSignals = signalOnCandles.Where(s => s.SignalType == SignalType.Context).ToList();
// Context validation - evaluates market conditions based on confidence levels
if (!ValidateContextStrategies(strategies, contextStrategies, config))
if (!ValidateContextStrategies(strategies, contextSignals, config))
{
return null; // Context strategies are blocking the trade
}
// Trend analysis - evaluate overall market direction
var trendDirection = EvaluateTrendDirection(trendStrategies, config);
// Check for 100% agreement across ALL signals (no threshold voting)
var allDirectionalSignals = signalOnCandles
.Where(s => s.Direction != TradeDirection.None && s.SignalType != SignalType.Context).ToList();
// Signal analysis - evaluate entry signals
var signalDirection = EvaluateSignalDirection(signalStrategies, config);
if (!allDirectionalSignals.Any())
{
return null; // No directional signals available
}
// Determine final direction and confidence
var (finalDirection, confidence) =
DetermineFinalSignal(signalDirection, trendDirection, signalStrategies, trendStrategies, config);
// Require 100% agreement - all signals must have the same direction
var lastSignalDirection = allDirectionalSignals.Last().Direction;
if (!allDirectionalSignals.All(s => s.Direction == lastSignalDirection))
{
return null; // Signals are not in complete agreement
}
if (finalDirection == TradeDirection.None || confidence < config.MinimumConfidence)
var finalDirection = lastSignalDirection;
// Calculate confidence based on the average confidence of all signals
var averageConfidence = CalculateAverageConfidence(allDirectionalSignals);
if (finalDirection == TradeDirection.None || averageConfidence < config.MinimumConfidence)
{
return null; // No valid signal or below minimum confidence
}
// Create composite signal
var lastSignal = signalStrategies.LastOrDefault() ??
trendStrategies.LastOrDefault() ?? contextStrategies.LastOrDefault();
var lastSignal = signals.LastOrDefault() ??
trendSignals.LastOrDefault() ?? contextSignals.LastOrDefault();
return new Signal(
ticker,
finalDirection,
confidence,
averageConfidence,
lastSignal?.Candle,
lastSignal?.Date ?? DateTime.UtcNow,
lastSignal?.Exchange ?? config.DefaultExchange,
IndicatorType.Composite,
SignalType.Signal);
SignalType.Signal, "Aggregated");
}
/// <summary>
/// Calculates the average confidence level from a list of signals
/// </summary>
private static Confidence CalculateAverageConfidence(List<Signal> signals)
{
if (!signals.Any())
{
return Confidence.None;
}
// Convert confidence enum to numeric values for averaging
var confidenceValues = signals.Select(s => (int)s.Confidence).ToList();
var averageValue = confidenceValues.Average();
// Round to nearest confidence level
var roundedValue = Math.Round(averageValue);
// Ensure the value is within valid confidence enum range
roundedValue = Math.Max(0, Math.Min(3, roundedValue));
return (Confidence)(int)roundedValue;
}
/// <summary>

View File

@@ -73,7 +73,7 @@ public class StDevContext : Indicator
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
var test = new IndicatorsResultBase()
{
@@ -119,7 +119,7 @@ public class StDevContext : Indicator
candleSignal,
candleSignal.Date,
candleSignal.Exchange,
Type, SignalType);
Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -17,7 +17,7 @@ namespace Managing.Domain.Strategies
FixedSizeQueue<Candle> Candles { get; set; }
List<Signal> Run();
IndicatorsResultBase GetStrategyValues();
IndicatorsResultBase GetIndicatorValues();
void UpdateCandles(HashSet<Candle> newCandles);
string GetName();
}

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Candles;
using Managing.Domain.Scenarios;
@@ -19,7 +20,7 @@ namespace Managing.Domain.Strategies
}
public string Name { get; set; }
[JsonIgnore] public FixedSizeQueue<Candle> Candles { get; set; }
[JsonIgnore] [IgnoreDataMember] public FixedSizeQueue<Candle> Candles { get; set; }
public IndicatorType Type { get; set; }
public SignalType SignalType { get; set; }
public int MinimumHistory { get; set; }
@@ -38,7 +39,7 @@ namespace Managing.Domain.Strategies
return new List<Signal>();
}
public virtual IndicatorsResultBase GetStrategyValues()
public virtual IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase();
}

View File

@@ -21,9 +21,11 @@ namespace Managing.Domain.Strategies
[Required] public IndicatorType IndicatorType { get; set; }
[Required] public SignalType SignalType { get; set; }
public User User { get; set; }
[Required] public string IndicatorName { get; set; }
public Signal(Ticker ticker, TradeDirection direction, Confidence confidence, Candle candle, DateTime date,
TradingExchanges exchange, IndicatorType indicatorType, SignalType signalType, User user = null)
TradingExchanges exchange, IndicatorType indicatorType, SignalType signalType, string indicatorName,
User user = null)
{
Direction = direction;
Confidence = confidence;
@@ -34,10 +36,11 @@ namespace Managing.Domain.Strategies
Status = SignalStatus.WaitingForPosition;
IndicatorType = indicatorType;
User = user;
IndicatorName = indicatorName;
SignalType = signalType;
Identifier =
$"{IndicatorType}-{direction}-{ticker}-{candle?.Close.ToString(CultureInfo.InvariantCulture)}-{date:yyyyMMdd-HHmmss}";
SignalType = signalType;
$"{indicatorName}-{indicatorType}-{direction}-{ticker}-{candle?.Close.ToString(CultureInfo.InvariantCulture)}-{date:yyyyMMdd-HHmmss}";
}
public void SetConfidence(Confidence confidence)

View File

@@ -40,7 +40,7 @@ public class ChandelierExitIndicator : Indicator
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase()
{
@@ -113,7 +113,8 @@ public class ChandelierExitIndicator : Indicator
candleSignal,
candleSignal.Date,
candleSignal.Exchange,
Type, SignalType);
Type, SignalType,
Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -19,7 +19,7 @@ public class DualEmaCrossIndicator : EmaBaseIndicator
MinimumHistory = Math.Max(fastPeriod, slowPeriod) * 2;
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase()
{
@@ -104,7 +104,7 @@ public class DualEmaCrossIndicator : EmaBaseIndicator
private void AddSignal(CandleDualEma candleSignal, TradeDirection direction, Confidence confidence)
{
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -16,7 +16,7 @@ public class EmaCrossIndicator : EmaBaseIndicator
Period = period;
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase()
{
@@ -68,7 +68,7 @@ public class EmaCrossIndicator : EmaBaseIndicator
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
{
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -89,7 +89,7 @@ public class LaggingSTC : Indicator
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
return new IndicatorsResultBase
@@ -130,7 +130,8 @@ public class LaggingSTC : Indicator
candleSignal,
candleSignal.Date,
candleSignal.Exchange,
Type, SignalType);
Type, SignalType,
Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -59,7 +59,7 @@ public class MacdCrossIndicator : Indicator
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase()
{
@@ -96,7 +96,7 @@ public class MacdCrossIndicator : Indicator
Confidence confidence)
{
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -49,7 +49,7 @@ public class RsiDivergenceConfirmIndicator : Indicator
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase()
{
@@ -233,7 +233,7 @@ public class RsiDivergenceConfirmIndicator : Indicator
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, Confidence confidence)
{
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -52,7 +52,7 @@ public class RsiDivergenceIndicator : Indicator
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase()
{
@@ -206,7 +206,7 @@ public class RsiDivergenceIndicator : Indicator
private void AddSignal(CandleRsi candleSignal, TradeDirection direction)
{
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, Confidence.Low,
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (Signals.Count(s => s.Identifier == signal.Identifier) < 1)
{

View File

@@ -64,7 +64,7 @@ public class StcIndicator : Indicator
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
if (FastPeriods != null && SlowPeriods != null)
{
@@ -110,7 +110,8 @@ public class StcIndicator : Indicator
candleSignal,
candleSignal.Date,
candleSignal.Exchange,
Type, SignalType);
Type, SignalType,
Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -157,7 +157,7 @@ public class SuperTrendCrossEma : Indicator
return superTrends;
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase()
{
@@ -171,7 +171,7 @@ public class SuperTrendCrossEma : Indicator
{
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
candleSignal, candleSignal.Date,
candleSignal.Exchange, Type, SignalType);
candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -61,7 +61,7 @@ public class SuperTrendIndicator : Indicator
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase()
{
@@ -99,7 +99,7 @@ public class SuperTrendIndicator : Indicator
{
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
candleSignal, candleSignal.Date,
candleSignal.Exchange, Type, SignalType);
candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -52,7 +52,7 @@ namespace Managing.Domain.Strategies.Signals
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
throw new NotImplementedException();
}

View File

@@ -54,7 +54,7 @@ public class EmaTrendIndicator : EmaBaseIndicator
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase()
{
@@ -65,7 +65,7 @@ public class EmaTrendIndicator : EmaBaseIndicator
public void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
{
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -65,7 +65,7 @@ public class StochRsiTrendIndicator : Indicator
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase()
{
@@ -108,7 +108,8 @@ public class StochRsiTrendIndicator : Indicator
candleSignal.Date,
candleSignal.Exchange,
Type,
SignalType);
SignalType,
Name);
if (!Signals.Any(s => s.Identifier == signal.Identifier))
{
Signals.AddItem(signal);

View File

@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents a miner on the Synth API leaderboard
/// </summary>
public class MinerInfo
{
[JsonPropertyName("coldkey")]
public string Coldkey { get; set; }
[JsonPropertyName("emission")]
public decimal Emission { get; set; }
[JsonPropertyName("incentive")]
public decimal Incentive { get; set; }
[JsonPropertyName("neuron_uid")]
public int NeuronUid { get; set; }
[JsonPropertyName("pruning_score")]
public decimal PruningScore { get; set; }
/// <summary>
/// Rank value from API (decimal representing the ranking score)
/// </summary>
[JsonPropertyName("rank")]
public decimal Rank { get; set; }
[JsonPropertyName("stake")]
public decimal Stake { get; set; }
[JsonPropertyName("updated_at")]
public string UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents the prediction data from a single miner
/// Contains multiple simulated price paths and the miner's information
/// </summary>
public class MinerPrediction
{
public string Asset { get; set; }
[JsonPropertyName("miner_uid")] public int MinerUid { get; set; }
[JsonPropertyName("num_simulations")] public int NumSimulations { get; set; }
public List<List<PricePoint>> Prediction { get; set; } = new();
[JsonPropertyName("start_time")] public string StartTime { get; set; }
[JsonPropertyName("time_increment")] public int TimeIncrement { get; set; }
[JsonPropertyName("time_length")] public int TimeLength { get; set; }
/// <summary>
/// Complete miner information including rank, stake, incentive, etc.
/// This is populated after fetching predictions by mapping MinerUid to MinerInfo.NeuronUid
/// </summary>
public MinerInfo? MinerInfo { get; set; }
/// <summary>
/// Converts the StartTime string to DateTime for easier manipulation
/// </summary>
public DateTime GetStartDateTime()
{
return DateTime.TryParse(StartTime, out var dateTime) ? dateTime : DateTime.MinValue;
}
}

View File

@@ -0,0 +1,18 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents a price at a specific time within a simulated path
/// </summary>
public class PricePoint
{
public decimal Price { get; set; }
public string Time { get; set; }
/// <summary>
/// Converts the Time string to DateTime for easier manipulation
/// </summary>
public DateTime GetDateTime()
{
return DateTime.TryParse(Time, out var dateTime) ? dateTime : DateTime.MinValue;
}
}

View File

@@ -0,0 +1,403 @@
using Managing.Domain.Risk;
using static Managing.Common.Enums;
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Result of Synth signal validation containing comprehensive analysis data
/// </summary>
public class SignalValidationResult
{
/// <summary>
/// Overall confidence level of the signal based on TP vs SL probability analysis
/// </summary>
public Confidence Confidence { get; set; }
/// <summary>
/// Raw stop loss probability (0.0 to 1.0)
/// </summary>
public decimal StopLossProbability { get; set; }
/// <summary>
/// Raw take profit probability (0.0 to 1.0)
/// </summary>
public decimal TakeProfitProbability { get; set; }
/// <summary>
/// Calculated ratio of Take Profit Probability / Stop Loss Probability
/// Higher values indicate more favorable risk/reward
/// </summary>
public decimal TpSlRatio { get; set; }
/// <summary>
/// Indicates if the signal should be blocked based on risk analysis
/// True when confidence is None or adverse probability is too high
/// </summary>
public bool IsBlocked { get; set; }
/// <summary>
/// Threshold used for adverse probability evaluation
/// </summary>
public decimal AdverseProbabilityThreshold { get; set; }
/// <summary>
/// Additional context information about the validation
/// </summary>
public string ValidationContext { get; set; } = string.Empty;
/// <summary>
/// Time horizon used for the probability calculations (in seconds)
/// </summary>
public int TimeHorizonSeconds { get; set; }
/// <summary>
/// Whether custom thresholds were used in the analysis
/// </summary>
public bool UsedCustomThresholds { get; set; }
/// <summary>
/// Monetary gain if take profit is reached (positive value)
/// </summary>
public decimal TakeProfitGain { get; set; }
/// <summary>
/// Monetary loss if stop loss is hit (positive value representing loss amount)
/// </summary>
public decimal StopLossLoss { get; set; }
/// <summary>
/// Expected Monetary Value: (TP_Gain * TP_Prob) - (SL_Loss * SL_Prob)
/// Positive values indicate favorable expected outcomes
/// </summary>
public decimal ExpectedMonetaryValue { get; set; }
/// <summary>
/// Expected Utility using logarithmic utility function for risk-adjusted decision making
/// Higher values indicate more desirable risk-adjusted outcomes
/// </summary>
public decimal ExpectedUtility { get; set; }
/// <summary>
/// Risk-adjusted return ratio (Expected Utility / Risk)
/// Higher values indicate better risk-adjusted opportunities
/// </summary>
public decimal UtilityRiskRatio { get; set; }
/// <summary>
/// Kelly Criterion fraction - optimal percentage of capital to allocate (0.0 to 1.0)
/// Based on Kelly formula: f* = (bp - q) / b, where b = payoff ratio, p = win probability, q = loss probability
/// Values above 0.25 (25%) are typically capped for practical risk management
/// </summary>
public decimal KellyFraction { get; set; }
/// <summary>
/// Capped Kelly Fraction for practical risk management (typically max 25% of capital)
/// </summary>
public decimal KellyCappedFraction { get; set; }
/// <summary>
/// Win/Loss ratio used in Kelly calculation (TakeProfitGain / StopLossLoss)
/// </summary>
public decimal WinLossRatio { get; set; }
/// <summary>
/// Kelly Criterion assessment indicating the quality of the opportunity
/// </summary>
public string KellyAssessment { get; set; } = string.Empty;
/// <summary>
/// Risk tolerance level affecting overall risk calculations
/// </summary>
public RiskToleranceLevel RiskTolerance { get; set; } = RiskToleranceLevel.Moderate;
/// <summary>
/// Whether to use Expected Utility Theory for decision making
/// </summary>
public bool UseExpectedUtility { get; set; } = true;
/// <summary>
/// Whether to use Kelly Criterion for position sizing recommendations
/// </summary>
public bool UseKellyCriterion { get; set; } = true;
/// <summary>
/// Trading balance used for utility calculations (from TradingBotConfig.BotTradingBalance)
/// Represents the actual capital allocated to this trading bot
/// </summary>
public decimal TradingBalance { get; private set; } = 10000m;
/// <summary>
/// Risk aversion parameter used for utility calculations (configured from RiskManagement)
/// </summary>
public decimal ConfiguredRiskAversion { get; private set; } = 1.0m;
/// <summary>
/// Calculates Expected Monetary Value and Expected Utility using configured risk parameters
/// </summary>
/// <param name="tradingBalance">Actual trading balance allocated to the bot</param>
/// <param name="riskConfig">Complete risk management configuration</param>
public void CalculateExpectedMetrics(decimal tradingBalance, RiskManagement riskConfig)
{
// Store configured values for reference
TradingBalance = tradingBalance;
ConfiguredRiskAversion = riskConfig.RiskAversion;
// Calculate Expected Monetary Value
// EMV = (TP_Gain * TP_Prob) - (SL_Loss * SL_Prob)
ExpectedMonetaryValue = (TakeProfitGain * TakeProfitProbability) - (StopLossLoss * StopLossProbability);
// Calculate Expected Utility using logarithmic utility function
// This accounts for diminishing marginal utility and risk aversion
ExpectedUtility = CalculateLogarithmicExpectedUtility();
// Calculate utility-to-risk ratio for ranking opportunities
var totalRisk = StopLossLoss > 0 ? StopLossLoss : 1m; // Avoid division by zero
UtilityRiskRatio = ExpectedUtility / totalRisk;
// Calculate Kelly Criterion for optimal position sizing using full risk config
CalculateKellyCriterion(riskConfig);
}
/// <summary>
/// Calculates Expected Utility using logarithmic utility function
/// U(x) = ln(tradingBalance + x) for gains, ln(tradingBalance - x) for losses
/// Uses the actual trading balance and configured risk aversion
/// </summary>
/// <returns>Expected utility value</returns>
private decimal CalculateLogarithmicExpectedUtility()
{
try
{
// Use actual trading balance and configured risk aversion
var baseCapital = TradingBalance > 0 ? TradingBalance : 10000m;
var riskAversion = ConfiguredRiskAversion > 0 ? ConfiguredRiskAversion : 1.0m;
// Calculate utility of TP outcome: U(tradingBalance + gain)
var tpOutcome = baseCapital + TakeProfitGain;
var tpUtility = tpOutcome > 0 ? (decimal)Math.Log((double)tpOutcome) / riskAversion : decimal.MinValue;
// Calculate utility of SL outcome: U(tradingBalance - loss)
var slOutcome = baseCapital - StopLossLoss;
var slUtility = slOutcome > 0 ? (decimal)Math.Log((double)slOutcome) / riskAversion : decimal.MinValue;
// Calculate utility of no-change outcome (neither TP nor SL hit)
var noChangeProb = Math.Max(0m, 1m - TakeProfitProbability - StopLossProbability);
var noChangeUtility = (decimal)Math.Log((double)baseCapital) / riskAversion;
// Expected Utility = Sum of (Utility * Probability) for all outcomes
var expectedUtility = (tpUtility * TakeProfitProbability) +
(slUtility * StopLossProbability) +
(noChangeUtility * noChangeProb);
return expectedUtility;
}
catch (Exception)
{
// Return conservative utility value on calculation errors
return decimal.MinValue;
}
}
/// <summary>
/// Calculates Kelly Criterion for optimal position sizing
/// Kelly Formula: f* = (bp - q) / b
/// Where: b = payoff ratio (win/loss), p = win probability, q = loss probability
/// </summary>
/// <param name="riskConfig">Complete risk management configuration</param>
private void CalculateKellyCriterion(RiskManagement riskConfig)
{
try
{
// Calculate Win/Loss Ratio (b in Kelly formula)
WinLossRatio = StopLossLoss > 0 ? TakeProfitGain / StopLossLoss : 0m;
// Handle edge cases
if (WinLossRatio <= 0 || TakeProfitProbability <= 0)
{
KellyFraction = 0m;
KellyCappedFraction = 0m;
KellyAssessment = "No Position - Unfavorable risk/reward ratio";
return;
}
// Kelly Formula: f* = (bp - q) / b
// Where:
// b = WinLossRatio (TakeProfitGain / StopLossLoss)
// p = TakeProfitProbability
// q = StopLossProbability
var numerator = (WinLossRatio * TakeProfitProbability) - StopLossProbability;
var kellyFraction = numerator / WinLossRatio;
// Ensure Kelly fraction is not negative (would indicate unfavorable bet)
KellyFraction = Math.Max(0m, kellyFraction);
// Apply fractional Kelly multiplier
KellyFraction *= riskConfig.KellyFractionalMultiplier;
// Apply practical cap for risk management
KellyCappedFraction = Math.Min(KellyFraction, riskConfig.KellyMaximumCap);
// Generate Kelly assessment using the configured threshold
KellyAssessment = GenerateKellyAssessment(riskConfig);
}
catch (Exception)
{
// Safe defaults on calculation errors
KellyFraction = 0m;
KellyCappedFraction = 0m;
WinLossRatio = 0m;
KellyAssessment = "Calculation Error - No position recommended";
}
}
/// <summary>
/// Generates a descriptive assessment of the Kelly Criterion result
/// </summary>
/// <param name="riskConfig">Risk management configuration containing Kelly thresholds</param>
/// <returns>Human-readable Kelly assessment</returns>
private string GenerateKellyAssessment(RiskManagement riskConfig)
{
if (KellyFraction <= 0)
return "No Position - Negative or zero Kelly fraction";
if (KellyFraction < riskConfig.KellyMinimumThreshold)
return $"Below Threshold - Kelly {KellyFraction:P2} < {riskConfig.KellyMinimumThreshold:P2} minimum";
if (KellyFraction < 0.05m) // 1-5%
return "Small Position - Low but positive edge";
if (KellyFraction < 0.10m) // 5-10%
return "Moderate Position - Reasonable edge";
if (KellyFraction < 0.25m) // 10-25%
return "Large Position - Strong edge detected";
if (KellyFraction < 0.50m) // 25-50%
return "Very Large Position - Exceptional edge (CAPPED for safety)";
// Above 50%
return "Extreme Position - Extraordinary edge (HEAVILY CAPPED for safety)";
}
/// <summary>
/// Gets detailed Kelly Criterion analysis including fractional betting recommendations
/// </summary>
/// <param name="totalCapital">Total available capital for position sizing</param>
/// <returns>Detailed Kelly analysis with dollar amounts</returns>
public string GetDetailedKellyAnalysis(decimal totalCapital = 100000m)
{
var recommendedAmount = KellyCappedFraction * totalCapital;
var uncappedAmount = KellyFraction * totalCapital;
var analysis = $"Kelly Analysis:\n" +
$"• Win/Loss Ratio: {WinLossRatio:F2}:1\n" +
$"• Optimal Kelly %: {KellyFraction:P2}\n" +
$"• Capped Kelly %: {KellyCappedFraction:P2}\n" +
$"• Recommended Amount: ${recommendedAmount:N0}\n";
if (KellyFraction > KellyCappedFraction)
{
analysis += $"• Uncapped Amount: ${uncappedAmount:N0} (RISK WARNING)\n";
}
analysis += $"• Assessment: {KellyAssessment}";
return analysis;
}
/// <summary>
/// Calculates fractional Kelly betting for more conservative position sizing
/// </summary>
/// <param name="fraction">Fraction of Kelly to use (e.g., 0.5 for half-Kelly)</param>
/// <returns>Fractional Kelly allocation percentage</returns>
public decimal GetFractionalKelly(decimal fraction = 0.5m)
{
if (fraction < 0 || fraction > 1) fraction = 0.5m; // Default to half-Kelly
return KellyFraction * fraction;
}
/// <summary>
/// Validates if the Kelly Criterion suggests this is a profitable opportunity
/// </summary>
/// <param name="riskConfig">Risk management configuration containing Kelly thresholds</param>
/// <returns>True if Kelly fraction is above the configured threshold</returns>
public bool IsKellyFavorable(RiskManagement riskConfig)
{
return KellyFraction > riskConfig.KellyMinimumThreshold;
}
/// <summary>
/// Alternative utility calculation using square root utility (less risk-averse than logarithmic)
/// Uses the actual trading balance from the bot configuration
/// </summary>
/// <returns>Expected utility using square root function</returns>
public decimal CalculateSquareRootExpectedUtility()
{
try
{
var baseCapital = TradingBalance > 0 ? TradingBalance : 10000m;
// Square root utility: U(x) = sqrt(x)
var tpOutcome = baseCapital + TakeProfitGain;
var tpUtility = tpOutcome > 0 ? (decimal)Math.Sqrt((double)tpOutcome) : 0m;
var slOutcome = Math.Max(0m, baseCapital - StopLossLoss);
var slUtility = (decimal)Math.Sqrt((double)slOutcome);
var noChangeProb = Math.Max(0m, 1m - TakeProfitProbability - StopLossProbability);
var noChangeUtility = (decimal)Math.Sqrt((double)baseCapital);
return (tpUtility * TakeProfitProbability) +
(slUtility * StopLossProbability) +
(noChangeUtility * noChangeProb);
}
catch (Exception)
{
return 0m;
}
}
/// <summary>
/// Gets a risk assessment based on Expected Utility Theory
/// </summary>
/// <returns>Descriptive risk assessment</returns>
public string GetUtilityRiskAssessment()
{
if (ExpectedMonetaryValue > 0 && ExpectedUtility > 0)
return "Favorable - Positive expected value and utility";
if (ExpectedMonetaryValue > 0 && ExpectedUtility <= 0)
return "Cautious - Positive expected value but negative risk-adjusted utility";
if (ExpectedMonetaryValue <= 0 && ExpectedUtility > 0)
return "Risk-Seeking - Negative expected value but positive utility (unusual)";
return "Unfavorable - Negative expected value and utility";
}
/// <summary>
/// Creates a result indicating Synth is disabled
/// </summary>
public static SignalValidationResult CreateDisabledResult(Confidence originalConfidence)
{
return new SignalValidationResult
{
Confidence = originalConfidence,
IsBlocked = false,
ValidationContext = "Synth API disabled"
};
}
/// <summary>
/// Creates a result for error scenarios
/// </summary>
public static SignalValidationResult CreateErrorResult(Confidence fallbackConfidence, string errorContext)
{
return new SignalValidationResult
{
Confidence = fallbackConfidence,
IsBlocked = false,
ValidationContext = $"Error in validation: {errorContext}"
};
}
}

View File

@@ -0,0 +1,64 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Configuration settings for Synth API integration
/// </summary>
public class SynthConfiguration
{
/// <summary>
/// Whether to enable Synth API integration
/// </summary>
public bool IsEnabled { get; set; } = false;
/// <summary>
/// Number of top miners to fetch predictions from (default: 10)
/// </summary>
public int TopMinersCount { get; set; } = 10;
/// <summary>
/// Time increment in seconds for predictions (default: 300 = 5 minutes)
/// </summary>
public int TimeIncrement { get; set; } = 300;
/// <summary>
/// Default time length in seconds for predictions (default: 86400 = 24 hours)
/// </summary>
public int DefaultTimeLength { get; set; } = 86400;
/// <summary>
/// Maximum acceptable liquidation probability threshold (0.0 to 1.0)
/// If liquidation probability exceeds this, position opening may be blocked
/// </summary>
public decimal MaxLiquidationProbability { get; set; } = 0.10m; // 10%
/// <summary>
/// Cache duration for predictions in minutes (default: 5 minutes)
/// </summary>
public int PredictionCacheDurationMinutes { get; set; } = 5;
/// <summary>
/// Whether to use Synth predictions for position sizing adjustments
/// </summary>
public bool UseForPositionSizing { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for signal filtering
/// </summary>
public bool UseForSignalFiltering { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary>
public bool UseForDynamicStopLoss { get; set; } = true;
/// <summary>
/// Validates the configuration
/// </summary>
public bool IsValid()
{
return !IsEnabled || (TopMinersCount > 0 &&
TimeIncrement > 0 &&
DefaultTimeLength > 0 &&
MaxLiquidationProbability >= 0 && MaxLiquidationProbability <= 1);
}
}

View File

@@ -0,0 +1,57 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents a cached leaderboard entry for Synth miners
/// Used for MongoDB persistence to avoid repeated API calls
/// </summary>
public class SynthMinersLeaderboard
{
/// <summary>
/// Unique identifier for this leaderboard entry
/// </summary>
public string Id { get; set; }
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Time increment used for this leaderboard data
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Signal date for which this leaderboard was retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// List of miners in the leaderboard
/// </summary>
public List<MinerInfo> Miners { get; set; } = new();
/// <summary>
/// When this leaderboard data was created/stored
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Generates a cache key for this leaderboard entry
/// </summary>
public string GetCacheKey()
{
var key = $"{Asset}_{TimeIncrement}";
if (IsBacktest && SignalDate.HasValue)
{
key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}";
}
return key;
}
}

View File

@@ -0,0 +1,72 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents cached prediction data from Synth miners
/// Used for MongoDB persistence to avoid repeated API calls
/// </summary>
public class SynthMinersPredictions
{
/// <summary>
/// Unique identifier for this predictions entry
/// </summary>
public string Id { get; set; }
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Time increment used for these predictions
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Time length (horizon) for these predictions in seconds
/// </summary>
public int TimeLength { get; set; }
/// <summary>
/// Signal date for which these predictions were retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// List of miner UIDs these predictions are from
/// </summary>
public List<int> MinerUids { get; set; } = new();
/// <summary>
/// The actual prediction data from miners
/// </summary>
public List<MinerPrediction> Predictions { get; set; } = new();
/// <summary>
/// When this prediction data was fetched from the API
/// </summary>
public DateTime FetchedAt { get; set; }
/// <summary>
/// When this prediction data was created/stored
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Generates a cache key for this predictions entry
/// </summary>
public string GetCacheKey()
{
var key = $"{Asset}_{TimeIncrement}_{TimeLength}";
if (IsBacktest && SignalDate.HasValue)
{
key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}";
}
return key;
}
}

View File

@@ -0,0 +1,67 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents cached prediction data from a single Synth miner
/// Used for MongoDB persistence to avoid repeated API calls and reduce document size
/// </summary>
public class SynthPrediction
{
/// <summary>
/// Unique identifier for this prediction entry
/// </summary>
public string Id { get; set; }
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Miner UID that provided this prediction
/// </summary>
public int MinerUid { get; set; }
/// <summary>
/// Time increment used for this prediction
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Time length (horizon) for this prediction in seconds
/// </summary>
public int TimeLength { get; set; }
/// <summary>
/// Signal date for which this prediction was retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// The actual prediction data from the miner
/// </summary>
public MinerPrediction Prediction { get; set; }
/// <summary>
/// When this prediction data was created/stored
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Generates a cache key for this prediction entry
/// </summary>
public string GetCacheKey()
{
var key = $"{Asset}_{TimeIncrement}_{TimeLength}_{MinerUid}";
if (IsBacktest && SignalDate.HasValue)
{
key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}";
}
return key;
}
}

View File

@@ -0,0 +1,13 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Result of Synth risk monitoring
/// </summary>
public class SynthRiskResult
{
public decimal LiquidationProbability { get; set; }
public bool ShouldWarn { get; set; }
public bool ShouldAutoClose { get; set; }
public string WarningMessage { get; set; }
public string EmergencyMessage { get; set; }
}

View File

@@ -18,5 +18,6 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections
public IndicatorType Type { get; set; }
public SignalType SignalType { get; set; }
public UserDto User { get; set; }
public string IndicatorName { get; set; }
}
}

View File

@@ -0,0 +1,42 @@
using Managing.Infrastructure.Databases.MongoDb.Attributes;
using Managing.Infrastructure.Databases.MongoDb.Configurations;
namespace Managing.Infrastructure.Databases.MongoDb.Collections;
/// <summary>
/// MongoDB DTO for storing Synth miners leaderboard data
/// </summary>
[BsonCollection("SynthMinersLeaderboard")]
public class SynthMinersLeaderboardDto : Document
{
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Time increment used for this leaderboard data
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Signal date for which this leaderboard was retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// Serialized JSON of miners list
/// </summary>
public string MinersData { get; set; }
/// <summary>
/// Cache key for quick lookup
/// </summary>
public string CacheKey { get; set; }
}

View File

@@ -0,0 +1,57 @@
using Managing.Infrastructure.Databases.MongoDb.Attributes;
using Managing.Infrastructure.Databases.MongoDb.Configurations;
namespace Managing.Infrastructure.Databases.MongoDb.Collections;
/// <summary>
/// MongoDB DTO for storing Synth miners predictions data
/// </summary>
[BsonCollection("SynthMinersPredictions")]
public class SynthMinersPredictionsDto : Document
{
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Time increment used for these predictions
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Time length (horizon) for these predictions in seconds
/// </summary>
public int TimeLength { get; set; }
/// <summary>
/// Signal date for which these predictions were retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// Serialized JSON of miner UIDs list
/// </summary>
public string MinerUidsData { get; set; }
/// <summary>
/// Serialized JSON of predictions data
/// </summary>
public string PredictionsData { get; set; }
/// <summary>
/// When this prediction data was fetched from the API
/// </summary>
public DateTime FetchedAt { get; set; }
/// <summary>
/// Cache key for quick lookup
/// </summary>
public string CacheKey { get; set; }
}

View File

@@ -0,0 +1,52 @@
using Managing.Infrastructure.Databases.MongoDb.Attributes;
using Managing.Infrastructure.Databases.MongoDb.Configurations;
namespace Managing.Infrastructure.Databases.MongoDb.Collections;
/// <summary>
/// MongoDB DTO for storing individual Synth miner prediction data
/// </summary>
[BsonCollection("SynthPredictions")]
public class SynthPredictionDto : Document
{
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Miner UID that provided this prediction
/// </summary>
public int MinerUid { get; set; }
/// <summary>
/// Time increment used for this prediction
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Time length (horizon) for this prediction in seconds
/// </summary>
public int TimeLength { get; set; }
/// <summary>
/// Signal date for which this prediction was retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// Serialized JSON of the prediction data
/// </summary>
public string PredictionData { get; set; }
/// <summary>
/// Cache key for quick lookup
/// </summary>
public string CacheKey { get; set; }
}

View File

@@ -1,4 +1,5 @@
using Managing.Domain.Accounts;
using System.Text.Json;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
@@ -6,6 +7,7 @@ using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Synth.Models;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Managing.Domain.Workers;
@@ -371,7 +373,8 @@ public static class MongoMappers
Status = signal.Status,
Timeframe = signal.Timeframe,
Type = signal.IndicatorType,
User = signal.User != null ? Map(signal.User) : null
User = signal.User != null ? Map(signal.User) : null,
IndicatorName = signal.IndicatorName
};
}
@@ -387,6 +390,7 @@ public static class MongoMappers
TradingExchanges.Binance, //TODO FIXME When the signal status is modified from controller
bSignal.Type,
bSignal.SignalType,
bSignal.IndicatorName,
bSignal.User != null ? Map(bSignal.User) : null)
{
Status = bSignal.Status
@@ -744,7 +748,6 @@ public static class MongoMappers
{
User = Map(bot.User),
Identifier = bot.Identifier,
BotType = bot.BotType,
Data = bot.Data,
LastStatus = bot.LastStatus
};
@@ -758,7 +761,6 @@ public static class MongoMappers
{
User = Map(b.User),
Identifier = b.Identifier,
BotType = b.BotType,
Data = b.Data,
LastStatus = b.LastStatus
};
@@ -792,4 +794,143 @@ public static class MongoMappers
Direction = fundingRate.Direction
};
}
#region Synth
/// <summary>
/// Maps domain SynthMinersLeaderboard to MongoDB DTO
/// </summary>
internal static SynthMinersLeaderboardDto Map(SynthMinersLeaderboard leaderboard)
{
if (leaderboard == null) return null;
return new SynthMinersLeaderboardDto
{
Asset = leaderboard.Asset,
TimeIncrement = leaderboard.TimeIncrement,
SignalDate = leaderboard.SignalDate,
IsBacktest = leaderboard.IsBacktest,
MinersData = JsonSerializer.Serialize(leaderboard.Miners),
CacheKey = leaderboard.GetCacheKey()
};
}
/// <summary>
/// Maps MongoDB DTO to domain SynthMinersLeaderboard
/// </summary>
internal static SynthMinersLeaderboard Map(SynthMinersLeaderboardDto dto)
{
if (dto == null) return null;
var miners = string.IsNullOrEmpty(dto.MinersData)
? new List<MinerInfo>()
: JsonSerializer.Deserialize<List<MinerInfo>>(dto.MinersData) ?? new List<MinerInfo>();
return new SynthMinersLeaderboard
{
Id = dto.Id.ToString(),
Asset = dto.Asset,
TimeIncrement = dto.TimeIncrement,
SignalDate = dto.SignalDate,
IsBacktest = dto.IsBacktest,
Miners = miners,
CreatedAt = dto.CreatedAt
};
}
/// <summary>
/// Maps domain SynthMinersPredictions to MongoDB DTO
/// </summary>
internal static SynthMinersPredictionsDto Map(SynthMinersPredictions predictions)
{
if (predictions == null) return null;
return new SynthMinersPredictionsDto
{
Asset = predictions.Asset,
TimeIncrement = predictions.TimeIncrement,
TimeLength = predictions.TimeLength,
SignalDate = predictions.SignalDate,
IsBacktest = predictions.IsBacktest,
MinerUidsData = JsonSerializer.Serialize(predictions.MinerUids),
PredictionsData = JsonSerializer.Serialize(predictions.Predictions),
CacheKey = predictions.GetCacheKey()
};
}
/// <summary>
/// Maps MongoDB DTO to domain SynthMinersPredictions
/// </summary>
internal static SynthMinersPredictions Map(SynthMinersPredictionsDto dto)
{
if (dto == null) return null;
var minerUids = string.IsNullOrEmpty(dto.MinerUidsData)
? new List<int>()
: JsonSerializer.Deserialize<List<int>>(dto.MinerUidsData) ?? new List<int>();
var predictions = string.IsNullOrEmpty(dto.PredictionsData)
? new List<MinerPrediction>()
: JsonSerializer.Deserialize<List<MinerPrediction>>(dto.PredictionsData) ?? new List<MinerPrediction>();
return new SynthMinersPredictions
{
Id = dto.Id.ToString(),
Asset = dto.Asset,
TimeIncrement = dto.TimeIncrement,
TimeLength = dto.TimeLength,
SignalDate = dto.SignalDate,
IsBacktest = dto.IsBacktest,
MinerUids = minerUids,
Predictions = predictions,
CreatedAt = dto.CreatedAt
};
}
/// <summary>
/// Maps domain SynthPrediction to MongoDB DTO
/// </summary>
internal static SynthPredictionDto Map(SynthPrediction prediction)
{
if (prediction == null) return null;
return new SynthPredictionDto
{
Asset = prediction.Asset,
MinerUid = prediction.MinerUid,
TimeIncrement = prediction.TimeIncrement,
TimeLength = prediction.TimeLength,
SignalDate = prediction.SignalDate,
IsBacktest = prediction.IsBacktest,
PredictionData = JsonSerializer.Serialize(prediction.Prediction),
CacheKey = prediction.GetCacheKey()
};
}
/// <summary>
/// Maps MongoDB DTO to domain SynthPrediction
/// </summary>
internal static SynthPrediction Map(SynthPredictionDto dto)
{
if (dto == null) return null;
var prediction = string.IsNullOrEmpty(dto.PredictionData)
? null
: JsonSerializer.Deserialize<MinerPrediction>(dto.PredictionData);
return new SynthPrediction
{
Id = dto.Id.ToString(),
Asset = dto.Asset,
MinerUid = dto.MinerUid,
TimeIncrement = dto.TimeIncrement,
TimeLength = dto.TimeLength,
SignalDate = dto.SignalDate,
IsBacktest = dto.IsBacktest,
Prediction = prediction,
CreatedAt = dto.CreatedAt
};
}
#endregion
}

View File

@@ -0,0 +1,223 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Synth.Models;
using Managing.Infrastructure.Databases.MongoDb;
using Managing.Infrastructure.Databases.MongoDb.Abstractions;
using Managing.Infrastructure.Databases.MongoDb.Collections;
using Microsoft.Extensions.Logging;
namespace Managing.Infrastructure.Databases;
/// <summary>
/// Repository implementation for Synth-related data operations using MongoDB
/// Provides persistence for leaderboard and individual predictions data
/// </summary>
public class SynthRepository : ISynthRepository
{
private readonly IMongoRepository<SynthMinersLeaderboardDto> _leaderboardRepository;
private readonly IMongoRepository<SynthPredictionDto> _individualPredictionsRepository;
private readonly ILogger<SynthRepository> _logger;
public SynthRepository(
IMongoRepository<SynthMinersLeaderboardDto> leaderboardRepository,
IMongoRepository<SynthPredictionDto> individualPredictionsRepository,
ILogger<SynthRepository> logger)
{
_leaderboardRepository = leaderboardRepository;
_individualPredictionsRepository = individualPredictionsRepository;
_logger = logger;
}
/// <summary>
/// Gets cached leaderboard data by cache key
/// </summary>
public async Task<SynthMinersLeaderboard?> GetLeaderboardAsync(string cacheKey)
{
try
{
var dto = await _leaderboardRepository.FindOneAsync(x => x.CacheKey == cacheKey);
if (dto == null)
{
_logger.LogDebug($"🔍 **Synth Cache** - No leaderboard cache found for key: {cacheKey}");
return null;
}
var result = MongoMappers.Map(dto);
_logger.LogDebug($"📦 **Synth Cache** - Retrieved leaderboard from MongoDB for key: {cacheKey}");
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error retrieving leaderboard cache for key: {cacheKey}");
return null;
}
}
/// <summary>
/// Saves leaderboard data to MongoDB
/// </summary>
public async Task SaveLeaderboardAsync(SynthMinersLeaderboard leaderboard)
{
try
{
leaderboard.CreatedAt = DateTime.UtcNow;
var dto = MongoMappers.Map(leaderboard);
// Check if we already have this cache key and update instead of inserting
var existing = await _leaderboardRepository.FindOneAsync(x => x.CacheKey == dto.CacheKey);
if (existing != null)
{
dto.Id = existing.Id;
_leaderboardRepository.Update(dto);
_logger.LogDebug($"💾 **Synth Cache** - Updated leaderboard in MongoDB for key: {dto.CacheKey}");
}
else
{
await _leaderboardRepository.InsertOneAsync(dto);
_logger.LogDebug($"💾 **Synth Cache** - Saved new leaderboard to MongoDB for key: {dto.CacheKey}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error saving leaderboard cache for key: {leaderboard.GetCacheKey()}");
}
}
/// <summary>
/// Gets individual cached prediction data by asset, parameters, and miner UIDs
/// </summary>
public async Task<List<SynthPrediction>> GetIndividualPredictionsAsync(
string asset,
int timeIncrement,
int timeLength,
List<int> minerUids,
bool isBacktest,
DateTime? signalDate)
{
try
{
var results = new List<SynthPrediction>();
foreach (var minerUid in minerUids)
{
// Build cache key for individual prediction
var cacheKey = $"{asset}_{timeIncrement}_{timeLength}_{minerUid}";
if (isBacktest && signalDate.HasValue)
{
cacheKey += $"_backtest_{signalDate.Value:yyyy-MM-dd-HH}";
}
var dto = await _individualPredictionsRepository.FindOneAsync(x => x.CacheKey == cacheKey);
if (dto != null)
{
var prediction = MongoMappers.Map(dto);
if (prediction != null)
{
results.Add(prediction);
}
}
}
if (results.Any())
{
_logger.LogDebug($"📦 **Synth Individual Cache** - Retrieved {results.Count}/{minerUids.Count} individual predictions for {asset}");
}
else
{
_logger.LogDebug($"🔍 **Synth Individual Cache** - No individual predictions found for {asset}");
}
return results;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error retrieving individual predictions cache for asset: {asset}");
return new List<SynthPrediction>();
}
}
/// <summary>
/// Saves individual prediction data to MongoDB
/// </summary>
public async Task SaveIndividualPredictionAsync(SynthPrediction prediction)
{
try
{
prediction.CreatedAt = DateTime.UtcNow;
var dto = MongoMappers.Map(prediction);
// Check if we already have this cache key and update instead of inserting
var existing = await _individualPredictionsRepository.FindOneAsync(x => x.CacheKey == dto.CacheKey);
if (existing != null)
{
dto.Id = existing.Id;
_individualPredictionsRepository.Update(dto);
_logger.LogDebug($"💾 **Synth Individual Cache** - Updated individual prediction for miner {prediction.MinerUid}");
}
else
{
await _individualPredictionsRepository.InsertOneAsync(dto);
_logger.LogDebug($"💾 **Synth Individual Cache** - Saved new individual prediction for miner {prediction.MinerUid}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error saving individual prediction cache for miner {prediction.MinerUid}: {ex.Message}");
}
}
/// <summary>
/// Saves multiple individual predictions to MongoDB in batch
/// </summary>
public async Task SaveIndividualPredictionsAsync(List<SynthPrediction> predictions)
{
if (!predictions.Any())
{
return;
}
try
{
var saveTasks = new List<Task>();
foreach (var prediction in predictions)
{
// Save each prediction individually to handle potential conflicts
saveTasks.Add(SaveIndividualPredictionAsync(prediction));
}
await Task.WhenAll(saveTasks);
_logger.LogInformation($"💾 **Synth Individual Cache** - Successfully saved {predictions.Count} individual predictions to MongoDB");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error saving batch of {predictions.Count} individual predictions");
}
}
/// <summary>
/// Cleans up old cached data beyond the retention period
/// </summary>
public async Task CleanupOldDataAsync(int retentionDays = 30)
{
try
{
var cutoffDate = DateTime.UtcNow.AddDays(-retentionDays);
// Clean up old leaderboard data
await _leaderboardRepository.DeleteManyAsync(x => x.CreatedAt < cutoffDate);
// Clean up old individual predictions data
await _individualPredictionsRepository.DeleteManyAsync(x => x.CreatedAt < cutoffDate);
_logger.LogInformation($"🧹 **Synth Cache** - Cleaned up old Synth cache data older than {retentionDays} days");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error during cleanup of old Synth cache data");
}
}
}

View File

@@ -0,0 +1,540 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Synth;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Risk;
using Managing.Domain.Strategies;
using Managing.Domain.Synth.Models;
using Managing.Domain.Users;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
using Xunit.Abstractions;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Tests;
public class SynthPredictionTests
{
private readonly ITestOutputHelper _testOutputHelper;
public SynthPredictionTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
/// <summary>
/// Helper method to create a test signal with realistic candle data
/// </summary>
private static Signal CreateTestSignal(Ticker ticker, TradeDirection direction, decimal price,
DateTime? date = null)
{
var signalDate = date ?? DateTime.UtcNow;
var candle = new Candle
{
Date = signalDate,
Open = price * 0.999m,
High = price * 1.001m,
Low = price * 0.998m,
Close = price,
BaseVolume = 1000m,
QuoteVolume = price * 1000m
};
return new Signal(
ticker: ticker,
direction: direction,
confidence: Confidence.Medium, // Will be updated by validation
candle: candle,
date: signalDate,
exchange: TradingExchanges.GmxV2,
indicatorType: IndicatorType.Stc,
signalType: SignalType.Signal,
indicatorName: "TestIndicator",
user: new User { Name = "TestUser" }
);
}
[Fact]
public async Task GetProbabilityOfTargetPriceAsync_ShouldReturnValidProbability_ForBTC_RealAPI()
{
// Arrange - Static values for testing
const decimal currentBtcPrice = 102000m; // Current BTC price at $102k
const decimal takeProfitPrice = currentBtcPrice * 1.02m; // 2% TP = $104,040
const decimal stopLossPrice = currentBtcPrice * 0.99m; // 1% SL = $100,980
const int timeHorizonHours = 24; // 24 hour forecast
Console.WriteLine($"🚀 Starting Synth API Test for BTC at ${currentBtcPrice:N0}");
// Create real API client and service
var httpClient = new HttpClient();
var logger = new TestLogger<SynthPredictionService>();
var synthApiClient = new SynthApiClient(httpClient, new TestLogger<SynthApiClient>());
var mockSynthRepository = new Mock<ISynthRepository>();
var synthPredictionService = new SynthPredictionService(synthApiClient, mockSynthRepository.Object, logger);
// Create configuration for enabled Synth API
var config = new SynthConfiguration
{
IsEnabled = true,
TopMinersCount = 5, // Use fewer miners for faster testing
TimeIncrement = 300, // 5 minutes (supported by Synth API)
DefaultTimeLength = timeHorizonHours * 3600, // 24 hours in seconds
MaxLiquidationProbability = 0.10m,
PredictionCacheDurationMinutes = 1 // Short cache for testing
};
// Act & Assert - Test Take Profit probability (upward movement for LONG)
try
{
Console.WriteLine("🔍 Fetching Take Profit probability from Synth API...");
var takeProfitProbability = await synthPredictionService.GetProbabilityOfTargetPriceAsync(
asset: "BTC",
currentPrice: currentBtcPrice,
targetPrice: takeProfitPrice,
timeHorizonSeconds: timeHorizonHours * 3600,
isLongPosition: false, // For TP, we want upward movement (opposite of liquidation direction)
config: config);
Console.WriteLine($"🎯 Take Profit Analysis (2% gain):");
Console.WriteLine($"Current Price: ${currentBtcPrice:N0}");
Console.WriteLine($"Target Price: ${takeProfitPrice:N0}");
Console.WriteLine($"Probability: {takeProfitProbability:P2}");
Assert.True(takeProfitProbability >= 0m && takeProfitProbability <= 1m,
"Take profit probability should be between 0 and 1");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Take Profit test failed: {ex.Message}");
Console.WriteLine("⚠️ Skipping Take Profit test due to API issue");
}
// Act & Assert - Test Stop Loss probability (downward movement for LONG)
try
{
Console.WriteLine("\n🔍 Fetching Stop Loss probability from Synth API...");
var stopLossProbability = await synthPredictionService.GetProbabilityOfTargetPriceAsync(
asset: "BTC",
currentPrice: currentBtcPrice,
targetPrice: stopLossPrice,
timeHorizonSeconds: timeHorizonHours * 3600,
isLongPosition: true, // For SL in long position, we check downward movement
config: config);
Console.WriteLine($"🛑 Stop Loss Analysis (1% loss):");
Console.WriteLine($"Current Price: ${currentBtcPrice:N0}");
Console.WriteLine($"Stop Loss Price: ${stopLossPrice:N0}");
Console.WriteLine($"Liquidation Risk: {stopLossProbability:P2}");
Assert.True(stopLossProbability >= 0m && stopLossProbability <= 1m,
"Stop loss probability should be between 0 and 1");
// Risk assessment - typical risk thresholds
if (stopLossProbability > 0.20m)
{
Console.WriteLine("⚠️ HIGH RISK: Liquidation probability exceeds 20%");
}
else if (stopLossProbability > 0.10m)
{
Console.WriteLine("⚡ MODERATE RISK: Liquidation probability between 10-20%");
}
else
{
Console.WriteLine("✅ LOW RISK: Liquidation probability below 10%");
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ Stop Loss test failed: {ex.Message}");
Console.WriteLine("⚠️ Skipping Stop Loss test due to API issue");
}
Console.WriteLine($"\n📊 Money Management Summary:");
Console.WriteLine($"Position: LONG BTC");
Console.WriteLine($"Entry: ${currentBtcPrice:N0}");
Console.WriteLine($"Take Profit: ${takeProfitPrice:N0} (+2.00%)");
Console.WriteLine($"Stop Loss: ${stopLossPrice:N0} (-1.00%)");
Console.WriteLine($"Risk/Reward Ratio: 1:2");
Console.WriteLine($"Time Horizon: {timeHorizonHours} hours");
Console.WriteLine("🏁 Test completed!");
}
[Fact]
public async Task ValidateSignalAsync_ShouldUseCustomThresholds_ForSignalFiltering_RealAPI()
{
// Arrange - Static values for custom threshold testing
const decimal currentBtcPrice = 107300m; // Current BTC price at $105,700
Console.WriteLine($"🔧 Starting RiskManagement Configuration Test for BTC at ${currentBtcPrice:N0}");
// Create real API client and service
var httpClient = new HttpClient();
var logger = new TestLogger<SynthPredictionService>();
var synthApiClient = new SynthApiClient(httpClient, new TestLogger<SynthApiClient>());
var mockSynthRepository = new Mock<ISynthRepository>();
var synthPredictionService = new SynthPredictionService(synthApiClient, mockSynthRepository.Object, logger);
// Define test scenarios for both LONG and SHORT signals
var signalDirections = new[]
{
new { Direction = TradeDirection.Long, Name = "LONG" },
new { Direction = TradeDirection.Short, Name = "SHORT" }
};
// Define RiskManagement configurations to test
var riskConfigs = new[]
{
new
{
Name = "Default (Moderate)",
RiskConfig = new RiskManagement
{
AdverseProbabilityThreshold = 0.25m, // 25% - balanced threshold
FavorableProbabilityThreshold = 0.30m, // 30% - reasonable expectation
RiskAversion = 1.5m, // Moderate risk aversion
KellyMinimumThreshold = 0.02m, // 2% - practical minimum
KellyMaximumCap = 0.20m, // 20% - reasonable maximum
KellyFractionalMultiplier = 0.75m, // 75% of Kelly (conservative)
RiskTolerance = RiskToleranceLevel.Moderate
}
},
new
{
Name = "Conservative",
RiskConfig = new RiskManagement
{
AdverseProbabilityThreshold = 0.20m, // 20% - stricter threshold
FavorableProbabilityThreshold = 0.40m, // 40% - higher TP expectation
RiskAversion = 2.0m, // Higher risk aversion
KellyMinimumThreshold = 0.03m, // 3% - higher minimum
KellyMaximumCap = 0.15m, // 15% - lower maximum
KellyFractionalMultiplier = 0.50m, // 50% of Kelly (very conservative)
RiskTolerance = RiskToleranceLevel.Conservative
}
},
new
{
Name = "Aggressive",
RiskConfig = new RiskManagement
{
AdverseProbabilityThreshold = 0.35m, // 35% - more permissive
FavorableProbabilityThreshold = 0.25m, // 25% - lower TP barrier
RiskAversion = 1.0m, // Lower risk aversion
KellyMinimumThreshold = 0.01m, // 1% - lower minimum
KellyMaximumCap = 0.30m, // 30% - higher maximum
KellyFractionalMultiplier = 1.0m, // 100% of Kelly (full Kelly)
RiskTolerance = RiskToleranceLevel.Aggressive
}
},
new
{
Name = "Moderate-Plus",
RiskConfig = new RiskManagement
{
AdverseProbabilityThreshold = 0.30m, // 30% - slightly more permissive
FavorableProbabilityThreshold = 0.35m, // 35% - balanced expectation
RiskAversion = 1.2m, // Slightly less risk-averse
KellyMinimumThreshold = 0.015m, // 1.5% - practical minimum
KellyMaximumCap = 0.25m, // 25% - reasonable maximum
KellyFractionalMultiplier = 0.85m, // 85% of Kelly
RiskTolerance = RiskToleranceLevel.Moderate
}
},
new
{
Name = "Risk-Focused",
RiskConfig = new RiskManagement
{
AdverseProbabilityThreshold = 0.18m, // 18% - tight risk control
FavorableProbabilityThreshold = 0.45m, // 45% - high TP requirement
RiskAversion = 2.5m, // High risk aversion
KellyMinimumThreshold = 0.025m, // 2.5% - higher minimum
KellyMaximumCap = 0.12m, // 12% - very conservative maximum
KellyFractionalMultiplier = 0.40m, // 40% of Kelly (very conservative)
RiskTolerance = RiskToleranceLevel.Conservative
}
},
new
{
Name = "Ultra-Conservative",
RiskConfig = new RiskManagement
{
AdverseProbabilityThreshold = 0.16m, // 16% - very strict threshold (should trigger some LOWs)
FavorableProbabilityThreshold = 0.60m, // 60% - very high TP requirement
RiskAversion = 3.5m, // Very high risk aversion
KellyMinimumThreshold = 0.04m, // 4% - high minimum barrier
KellyMaximumCap = 0.08m, // 8% - very low maximum (forces heavy capping)
KellyFractionalMultiplier = 0.25m, // 25% of Kelly (ultra conservative)
RiskTolerance = RiskToleranceLevel.Conservative
}
},
new
{
Name =
"Paranoid-Blocking",
RiskConfig = new RiskManagement
{
AdverseProbabilityThreshold = 0.12m, // 12% - very strict (should block 22-25% SL signals)
FavorableProbabilityThreshold = 0.60m, // 60% - very high TP requirement
RiskAversion = 4.0m, // Extremely high risk aversion
KellyMinimumThreshold = 0.05m, // 5% - very high minimum
KellyMaximumCap = 0.06m, // 6% - extremely conservative maximum
KellyFractionalMultiplier = 0.15m, // 15% of Kelly (extremely conservative)
RiskTolerance = RiskToleranceLevel.Conservative,
SignalValidationTimeHorizonHours = 24
}
},
new
{
Name = "Extreme-Blocking",
RiskConfig = new RiskManagement
{
AdverseProbabilityThreshold = 0.08m, // 8% - extremely strict (will block 22-25% SL signals)
FavorableProbabilityThreshold = 0.70m, // 70% - extremely high TP requirement
RiskAversion = 5.0m, // Maximum risk aversion
KellyMinimumThreshold = 0.08m, // 8% - very high minimum
KellyMaximumCap = 0.05m, // 5% - extremely small maximum
KellyFractionalMultiplier = 0.10m, // 10% of Kelly (ultra-conservative)
RiskTolerance = RiskToleranceLevel.Conservative,
SignalValidationTimeHorizonHours = 24
}
}
};
// Store results for summary
var testResults =
new Dictionary<string, Dictionary<string, SignalValidationResult>>();
// Test each RiskManagement configuration with both LONG and SHORT signals
foreach (var configTest in riskConfigs)
{
Console.WriteLine($"\n📊 Testing {configTest.Name})");
testResults[configTest.Name] =
new Dictionary<string, SignalValidationResult>();
// Create bot configuration with the specific RiskManagement
var botConfig = new TradingBotConfig
{
BotTradingBalance = 50000m, // $50k trading balance for realistic utility calculations
Timeframe = Timeframe.FifteenMinutes,
UseSynthApi = true,
UseForSignalFiltering = true,
UseForPositionSizing = true,
UseForDynamicStopLoss = false,
RiskManagement = configTest.RiskConfig, // Use the specific risk configuration
MoneyManagement = new MoneyManagement
{
Name = "Test Money Management",
StopLoss = 0.02m, // 2% stop loss
TakeProfit = 0.022m, // 4% take profit (1:2 risk/reward ratio)
Leverage = 10m,
Timeframe = Timeframe.FifteenMinutes
}
};
foreach (var signal in signalDirections)
{
try
{
Console.WriteLine($" 🎯 {signal.Name} Signal Test");
// Create a test signal for this direction
var testSignal = CreateTestSignal(Ticker.BTC, signal.Direction, currentBtcPrice);
var result = await synthPredictionService.ValidateSignalAsync(
signal: testSignal,
currentPrice: currentBtcPrice,
botConfig: botConfig,
isBacktest: false,
customThresholds: null); // No custom thresholds - use RiskManagement config
testResults[configTest.Name][signal.Name] = result;
Console.WriteLine($" 🎯 Confidence: {result.Confidence}");
Console.WriteLine(
$" 📊 SL Risk: {result.StopLossProbability:P2} | TP Prob: {result.TakeProfitProbability:P2}");
Console.WriteLine(
$" 🎲 TP/SL Ratio: {result.TpSlRatio:F2}x | Win/Loss: {result.WinLossRatio:F2}:1");
Console.WriteLine($" 💰 Expected Value: ${result.ExpectedMonetaryValue:F2}");
Console.WriteLine($" 🧮 Expected Utility: {result.ExpectedUtility:F4}");
Console.WriteLine(
$" 🎯 Kelly: {result.KellyFraction:P2} (Capped: {result.KellyCappedFraction:P2})");
Console.WriteLine($" 📊 Kelly Assessment: {result.KellyAssessment}");
Console.WriteLine($" ✅ Kelly Favorable: {result.IsKellyFavorable(configTest.RiskConfig)}");
Console.WriteLine($" 🚫 Blocked: {result.IsBlocked}");
// Debug: Show actual probability values and threshold comparison
var adverseThreshold = configTest.RiskConfig.AdverseProbabilityThreshold;
Console.WriteLine(
$" 🔍 DEBUG - SL: {result.StopLossProbability:F4} | TP: {result.TakeProfitProbability:F4} | Threshold: {adverseThreshold:F4}");
Console.WriteLine(
$" 🔍 DEBUG - SL > Threshold: {result.StopLossProbability > adverseThreshold} | TP > SL: {result.TakeProfitProbability > result.StopLossProbability}");
// Assert that the method works with RiskManagement configuration
Assert.True(Enum.IsDefined(typeof(Confidence), result.Confidence),
$"{configTest.Name} - {signal.Name} signal should return a valid Confidence level");
// Assert that Kelly calculations were performed
Assert.True(result.KellyFraction >= 0, "Kelly fraction should be non-negative");
Assert.True(result.KellyCappedFraction >= 0, "Capped Kelly fraction should be non-negative");
// Assert that Expected Utility calculations were performed
Assert.True(result.TradingBalance > 0, "Trading balance should be set from bot config");
Assert.Equal(botConfig.BotTradingBalance, result.TradingBalance);
}
catch (Exception ex)
{
Console.WriteLine($" ❌ {signal.Name} signal test failed: {ex.Message}");
// Create a fallback result for error cases
testResults[configTest.Name][signal.Name] = new SignalValidationResult
{
Confidence = Confidence.High, // Default to high confidence on error
ValidationContext = $"Error: {ex.Message}"
};
}
}
}
// Display comprehensive results summary
Console.WriteLine($"\n📈 Comprehensive RiskManagement Configuration Test Summary:");
Console.WriteLine($"Asset: BTC | Price: ${currentBtcPrice:N0} | Trading Balance: ${50000:N0}");
Console.WriteLine($"Stop Loss: 2.0% | Take Profit: 4.0% | Risk/Reward Ratio: 1:2.0");
_testOutputHelper.WriteLine($"\n🎯 Results Matrix:");
_testOutputHelper.WriteLine(
$"{"Configuration",-20} {"LONG Confidence",-15} {"LONG Kelly",-12} {"SHORT Confidence",-16} {"SHORT Kelly",-12}");
_testOutputHelper.WriteLine(new string('-', 85));
foreach (var configTest in riskConfigs)
{
var longResult = testResults[configTest.Name].GetValueOrDefault("LONG");
var shortResult = testResults[configTest.Name].GetValueOrDefault("SHORT");
var longConf = longResult?.Confidence ?? Confidence.None;
var shortConf = shortResult?.Confidence ?? Confidence.None;
var longKelly = longResult?.KellyCappedFraction ?? 0m;
var shortKelly = shortResult?.KellyCappedFraction ?? 0m;
_testOutputHelper.WriteLine(
$"{configTest.Name,-20} {GetConfidenceDisplay(longConf),-15} {longKelly,-12:P1} {GetConfidenceDisplay(shortConf),-16} {shortKelly,-12:P1}");
}
// Display detailed ValidationContext for each configuration and direction
Console.WriteLine($"\n📊 Detailed Analysis Results:");
Console.WriteLine(new string('=', 120));
foreach (var configTest in riskConfigs)
{
Console.WriteLine($"\n🔧 {configTest.Name}");
Console.WriteLine(new string('-', 80));
var longResult = testResults[configTest.Name].GetValueOrDefault("LONG");
var shortResult = testResults[configTest.Name].GetValueOrDefault("SHORT");
if (longResult != null)
{
Console.WriteLine($"📈 LONG Signal Analysis:");
Console.WriteLine($" Context: {longResult.ValidationContext ?? "N/A"}");
Console.WriteLine(
$" Confidence: {GetConfidenceDisplay(longResult.Confidence)} | Blocked: {longResult.IsBlocked}");
Console.WriteLine(
$" Kelly Assessment: {longResult.KellyAssessment} | Kelly Favorable: {longResult.IsKellyFavorable(configTest.RiskConfig)}");
if (longResult.TradingBalance > 0)
{
Console.WriteLine(
$" Trading Balance: ${longResult.TradingBalance:N0} | Risk Assessment: {longResult.GetUtilityRiskAssessment()}");
}
}
else
{
Console.WriteLine($"📈 LONG Signal Analysis: ERROR - No result available");
}
Console.WriteLine(); // Empty line for separation
if (shortResult != null)
{
Console.WriteLine($"📉 SHORT Signal Analysis:");
Console.WriteLine($" Context: {shortResult.ValidationContext ?? "N/A"}");
Console.WriteLine(
$" Confidence: {GetConfidenceDisplay(shortResult.Confidence)} | Blocked: {shortResult.IsBlocked}");
Console.WriteLine(
$" Kelly Assessment: {shortResult.KellyAssessment} | Kelly Favorable: {shortResult.IsKellyFavorable(configTest.RiskConfig)}");
if (shortResult.TradingBalance > 0)
{
Console.WriteLine(
$" Trading Balance: ${shortResult.TradingBalance:N0} | Risk Assessment: {shortResult.GetUtilityRiskAssessment()}");
}
}
else
{
Console.WriteLine($"📉 SHORT Signal Analysis: ERROR - No result available");
}
}
Console.WriteLine($"\n📊 Risk Configuration Analysis:");
Console.WriteLine($"• Default: Balanced 20% adverse threshold, 1% Kelly minimum");
Console.WriteLine($"• Conservative: Strict 15% adverse, 2% Kelly min, half-Kelly multiplier");
Console.WriteLine($"• Aggressive: Permissive 30% adverse, 0.5% Kelly min, full Kelly");
Console.WriteLine($"• Custom Permissive: Very permissive 35% adverse, low barriers");
Console.WriteLine($"• Custom Strict: Very strict 10% adverse, high barriers, conservative sizing");
Console.WriteLine($"\n💡 Key Insights:");
Console.WriteLine($"• Conservative configs should block more signals (lower confidence)");
Console.WriteLine($"• Aggressive configs should allow more signals (higher confidence)");
Console.WriteLine($"• Kelly fractions should vary based on risk tolerance settings");
Console.WriteLine($"• Expected Utility should reflect trading balance and risk aversion");
// Verify that we have results for all configurations and directions
foreach (var configTest in riskConfigs)
{
foreach (var signal in signalDirections)
{
Assert.True(testResults.ContainsKey(configTest.Name) &&
testResults[configTest.Name].ContainsKey(signal.Name),
$"Should have test result for {configTest.Name} - {signal.Name}");
var result = testResults[configTest.Name][signal.Name];
Assert.NotNull(result);
Assert.True(result.TradingBalance > 0, "Trading balance should be populated from bot config");
}
}
Console.WriteLine("🏁 Comprehensive RiskManagement Configuration Test completed!");
}
/// <summary>
/// Helper method to display confidence levels with emojis
/// </summary>
/// <param name="confidence">Confidence level</param>
/// <returns>Formatted confidence display</returns>
private static string GetConfidenceDisplay(Confidence confidence)
{
return confidence switch
{
Confidence.High => "🟢 HIGH",
Confidence.Medium => "🟡 MEDIUM",
Confidence.Low => "🟠 LOW",
Confidence.None => "🔴 NONE",
_ => "❓ UNKNOWN"
};
}
}
// Simple test logger implementation
public class TestLogger<T> : ILogger<T>
{
public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter)
{
// Silent logger for tests - output goes to Console.WriteLine
}
}

View File

@@ -7,6 +7,25 @@ using NSwag.CodeGeneration.TypeScript;
var document = OpenApiDocument.FromUrlAsync(("http://localhost:5000/swagger/v1/swagger.json")).Result;
// Get the solution directory by going up from the current executable location
var currentDirectory = Directory.GetCurrentDirectory();
var solutionDirectory = currentDirectory;
// Navigate up until we find the src directory or reach a reasonable limit
for (int i = 0; i < 10; i++)
{
if (Directory.Exists(Path.Combine(solutionDirectory, "src")))
break;
var parent = Directory.GetParent(solutionDirectory);
if (parent == null)
break;
solutionDirectory = parent.FullName;
}
var targetDirectory = Path.Combine(solutionDirectory, "src", "Managing.WebApp", "src", "generated");
Directory.CreateDirectory(targetDirectory); // Ensure the directory exists
var settings = new TypeScriptClientGeneratorSettings
{
ClassName = "{controller}Client",
@@ -30,7 +49,27 @@ var settings = new TypeScriptClientGeneratorSettings
var generatorApiClient = new TypeScriptClientGenerator(document, settings);
var codeApiClient = generatorApiClient.GenerateFile();
File.WriteAllText("ManagingApi.ts", codeApiClient);
// Add the necessary imports after the auto-generated comment
var requiredImports = @"
import AuthorizedApiBase from ""./AuthorizedApiBase"";
import IConfig from ""./IConfig"";
";
// Find the end of the auto-generated comment and insert imports
var autoGeneratedEndIndex = codeApiClient.IndexOf("//----------------------");
if (autoGeneratedEndIndex != -1)
{
// Find the second occurrence (end of the comment block)
autoGeneratedEndIndex = codeApiClient.IndexOf("//----------------------", autoGeneratedEndIndex + 1);
if (autoGeneratedEndIndex != -1)
{
autoGeneratedEndIndex = codeApiClient.IndexOf("\n", autoGeneratedEndIndex) + 1;
codeApiClient = codeApiClient.Insert(autoGeneratedEndIndex, requiredImports);
}
}
File.WriteAllText(Path.Combine(targetDirectory, "ManagingApi.ts"), codeApiClient);
var settingsTypes = new TypeScriptClientGeneratorSettings
{
@@ -53,4 +92,4 @@ var settingsTypes = new TypeScriptClientGeneratorSettings
var generatorTypes = new TypeScriptClientGenerator(document, settingsTypes);
var codeTypes = generatorTypes.GenerateFile();
File.WriteAllText("ManagingApiTypes.ts", codeTypes);
File.WriteAllText(Path.Combine(targetDirectory, "ManagingApiTypes.ts"), codeTypes);

View File

@@ -1,5 +1,5 @@
# Use an official Node.js image as the base
FROM node:18-alpine
FROM node:22.14.0-alpine
# Set the working directory in the container
WORKDIR /app
@@ -8,37 +8,38 @@ WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
# Install git and Python
#RUN apk update && apk add --no-cache git python3 make g++
RUN apk update && apk add --no-cache git python3 make g++
# Create a symlink for python3 as python
#RUN ln -sf /usr/bin/python3 /usr/bin/python
# Create a symlink for python3 as python
# This might not be strictly necessary for your current issue but good to keep if Python scripts are involved.
# RUN ln -sf /usr/bin/python3 /usr/bin/python
# Copy package.json and package-lock.json to the container
# COPY package*.json ./
COPY /src/Managing.WebApp/package.json ./
# Copy package.json and package-lock.json to the container
# COPY package*.json ./
COPY /src/Managing.WebApp/package.json ./
# Install dependencies with the --legacy-peer-deps flag to bypass peer dependency conflicts
RUN npm install --legacy-peer-deps
RUN npm install -g tailwindcss postcss autoprefixer @tailwindcss/typography
# Install dependencies with the --legacy-peer-deps flag to bypass peer dependency conflicts
RUN npm install --legacy-peer-deps --loglevel verbose
RUN npm install -g tailwindcss postcss autoprefixer @tailwindcss/typography
# Copy the rest of the app's source code to the container
# COPY . .
RUN ls -la
COPY src/Managing.WebApp/ .
RUN node --max-old-space-size=8192 ./node_modules/.bin/vite build
# Copy the rest of the app's source code to the container
# COPY . .
RUN ls -la
COPY src/Managing.WebApp/ .
RUN node --max-old-space-size=8192 ./node_modules/.bin/vite build
# Build the app
RUN npm run build
# Build the app
RUN npm run build
# Use NGINX as the web server
FROM nginx:alpine
# Use NGINX as the web server
FROM nginx:alpine
# Copy the built app to the NGINX web server directory
# COPY --from=0 /app/build /usr/share/nginx/html
COPY --from=0 /app/dist /usr/share/nginx/html
# Copy the built app to the NGINX web server directory
# COPY --from=0 /app/build /usr/share/nginx/html
COPY --from=0 /app/dist /usr/share/nginx/html
# Expose port 80 for the NGINX web server
EXPOSE 80
# Expose port 80 for the NGINX web server
EXPOSE 80
# Start the NGINX web server
# Start the NGINX web server
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -32,7 +32,6 @@
"canonicalize": "^2.0.0",
"classnames": "^2.3.1",
"connectkit": "^1.8.2",
"crypto": "^1.0.1",
"date-fns": "^2.30.0",
"elliptic": "^6.6.1",
"jotai": "^1.6.7",

View File

@@ -13,6 +13,7 @@ import {
Ticker,
Timeframe,
TradingBotConfig,
TradingBotConfigRequest,
UpdateBotConfigRequest
} from '../../../generated/ManagingApi'
import Toast from '../Toast/Toast'
@@ -58,6 +59,11 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
customStopLoss: number
customTakeProfit: number
customLeverage: number
// Synth API fields
useSynthApi: boolean
useForPositionSizing: boolean
useForSignalFiltering: boolean
useForDynamicStopLoss: boolean
}>({
name: '',
accountName: '',
@@ -77,9 +83,16 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
useCustomMoneyManagement: false,
customStopLoss: 0.01,
customTakeProfit: 0.02,
customLeverage: 1
customLeverage: 1,
useSynthApi: false,
useForPositionSizing: true,
useForSignalFiltering: true,
useForDynamicStopLoss: true
})
// State for advanced parameters dropdown
const [showAdvancedParams, setShowAdvancedParams] = useState(false)
// Fetch data
const { data: accounts } = useQuery({
queryFn: async () => {
@@ -110,11 +123,11 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
if (mode === 'create' && backtest) {
// Initialize from backtest
setFormData({
name: `Bot-${backtest.config.scenarioName}-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}`,
name: `Bot-${backtest.config.scenarioName || 'Custom'}-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}`,
accountName: backtest.config.accountName,
moneyManagementName: moneyManagements?.[0]?.name || '',
ticker: backtest.config.ticker,
scenarioName: backtest.config.scenarioName,
scenarioName: backtest.config.scenarioName || '',
timeframe: backtest.config.timeframe,
isForWatchingOnly: false,
botTradingBalance: 1000,
@@ -128,7 +141,11 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
useCustomMoneyManagement: true, // Default to custom for backtests
customStopLoss: backtest.config.moneyManagement?.stopLoss || 0.01,
customTakeProfit: backtest.config.moneyManagement?.takeProfit || 0.02,
customLeverage: backtest.config.moneyManagement?.leverage || 1
customLeverage: backtest.config.moneyManagement?.leverage || 1,
useSynthApi: false,
useForPositionSizing: true,
useForSignalFiltering: true,
useForDynamicStopLoss: true
})
} else if (mode === 'update' && existingBot) {
// Initialize from existing bot
@@ -137,7 +154,7 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
accountName: existingBot.config.accountName,
moneyManagementName: existingBot.config.moneyManagement?.name || '',
ticker: existingBot.config.ticker,
scenarioName: existingBot.config.scenarioName,
scenarioName: existingBot.config.scenarioName || '',
timeframe: existingBot.config.timeframe,
isForWatchingOnly: existingBot.config.isForWatchingOnly,
botTradingBalance: existingBot.config.botTradingBalance,
@@ -151,7 +168,11 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
useCustomMoneyManagement: false,
customStopLoss: existingBot.config.moneyManagement?.stopLoss || 0.01,
customTakeProfit: existingBot.config.moneyManagement?.takeProfit || 0.02,
customLeverage: existingBot.config.moneyManagement?.leverage || 1
customLeverage: existingBot.config.moneyManagement?.leverage || 1,
useSynthApi: existingBot.config.useSynthApi || false,
useForPositionSizing: existingBot.config.useForPositionSizing || true,
useForSignalFiltering: existingBot.config.useForSignalFiltering || true,
useForDynamicStopLoss: existingBot.config.useForDynamicStopLoss || true
})
} else if (mode === 'create' && !backtest) {
// Initialize for new bot creation
@@ -174,7 +195,11 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
useCustomMoneyManagement: false,
customStopLoss: 0.01,
customTakeProfit: 0.02,
customLeverage: 1
customLeverage: 1,
useSynthApi: false,
useForPositionSizing: true,
useForSignalFiltering: true,
useForDynamicStopLoss: true
})
}
}, [mode, backtest, existingBot, accounts, moneyManagements, scenarios])
@@ -216,6 +241,17 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
}))
}
const handleSynthApiToggle = (enabled: boolean) => {
setFormData(prev => ({
...prev,
useSynthApi: enabled,
// Reset sub-options when main toggle is turned off
useForPositionSizing: enabled ? prev.useForPositionSizing : false,
useForSignalFiltering: enabled ? prev.useForSignalFiltering : false,
useForDynamicStopLoss: enabled ? prev.useForDynamicStopLoss : false
}))
}
const handleSubmit = async () => {
const t = new Toast(mode === 'create' ? 'Creating bot...' : 'Updating bot...')
const client = new BotClient({}, apiUrl)
@@ -249,30 +285,31 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
return
}
// Create TradingBotConfig (reused for both create and update)
const tradingBotConfig: TradingBotConfig = {
// Create TradingBotConfigRequest (instead of TradingBotConfig)
const tradingBotConfigRequest: TradingBotConfigRequest = {
accountName: formData.accountName,
ticker: formData.ticker,
scenarioName: formData.scenarioName,
scenarioName: formData.scenarioName || undefined,
timeframe: formData.timeframe,
botType: formData.botType,
isForWatchingOnly: formData.isForWatchingOnly,
isForBacktest: false,
cooldownPeriod: formData.cooldownPeriod,
maxLossStreak: formData.maxLossStreak,
maxPositionTimeHours: formData.maxPositionTimeHours,
flipOnlyWhenInProfit: formData.flipOnlyWhenInProfit,
flipPosition: formData.flipPosition,
name: formData.name,
botTradingBalance: formData.botTradingBalance,
moneyManagement: moneyManagement,
closeEarlyWhenProfitable: formData.closeEarlyWhenProfitable
closeEarlyWhenProfitable: formData.closeEarlyWhenProfitable,
useSynthApi: formData.useSynthApi,
useForPositionSizing: formData.useForPositionSizing,
useForSignalFiltering: formData.useForSignalFiltering,
useForDynamicStopLoss: formData.useForDynamicStopLoss
}
if (mode === 'create') {
// Create new bot
const request: StartBotRequest = {
config: tradingBotConfig,
config: tradingBotConfigRequest,
moneyManagementName: formData.useCustomMoneyManagement ? undefined : formData.moneyManagementName
}
@@ -282,7 +319,7 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
// Update existing bot
const request: UpdateBotConfigRequest = {
identifier: existingBot!.identifier,
config: tradingBotConfig,
config: tradingBotConfigRequest,
moneyManagementName: formData.useCustomMoneyManagement ? undefined : formData.moneyManagementName
}
@@ -373,7 +410,12 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
<div className="form-control">
<label className="label">
<span className="label-text">Timeframe</span>
<div className="flex items-center gap-2">
<span className="label-text">Timeframe</span>
<div className="tooltip tooltip-top" data-tip="Chart timeframe for analysis. Lower timeframes (1m, 5m) for scalping, higher timeframes (1h, 4h) for swing trading">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<select
className="select select-bordered"
@@ -390,7 +432,12 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
<div className="form-control">
<label className="label">
<span className="label-text">Bot Type</span>
<div className="flex items-center gap-2">
<span className="label-text">Bot Type</span>
<div className="tooltip tooltip-top" data-tip="ScalpingBot: Quick trades based on short-term signals. FlippingBot: Position reversal strategy that flips between long/short">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<select
className="select select-bordered"
@@ -408,7 +455,12 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
<div className="form-control">
<label className="label">
<span className="label-text">Trading Balance</span>
<div className="flex items-center gap-2">
<span className="label-text">Trading Balance</span>
<div className="tooltip tooltip-top" data-tip="Amount of capital allocated for this bot's trading activities. This determines maximum position sizes">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<input
type="number"
@@ -420,51 +472,14 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Cooldown Period (candles)</span>
</label>
<input
type="number"
className="input input-bordered"
value={formData.cooldownPeriod}
onChange={(e) => handleInputChange('cooldownPeriod', parseInt(e.target.value))}
min="1"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Max Loss Streak</span>
</label>
<input
type="number"
className="input input-bordered"
value={formData.maxLossStreak}
onChange={(e) => handleInputChange('maxLossStreak', parseInt(e.target.value))}
min="0"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Max Position Time (hours)</span>
</label>
<input
type="number"
className="input input-bordered"
value={formData.maxPositionTimeHours || ''}
onChange={(e) => handleInputChange('maxPositionTimeHours', e.target.value ? parseFloat(e.target.value) : null)}
min="0.1"
step="0.1"
placeholder="Optional"
/>
</div>
{/* Checkboxes */}
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">Watch Only Mode</span>
<div className="flex items-center gap-2">
<span className="label-text">Watch Only Mode</span>
<div className="tooltip tooltip-top" data-tip="Bot will analyze and generate signals but won't execute actual trades. Good for testing strategies">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
@@ -473,51 +488,161 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">Flip Only When In Profit</span>
<input
type="checkbox"
className="checkbox"
checked={formData.flipOnlyWhenInProfit}
onChange={(e) => handleInputChange('flipOnlyWhenInProfit', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">Enable Position Flipping</span>
<input
type="checkbox"
className="checkbox"
checked={formData.flipPosition}
onChange={(e) => handleInputChange('flipPosition', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">Close Early When Profitable</span>
<input
type="checkbox"
className="checkbox"
checked={formData.closeEarlyWhenProfitable}
onChange={(e) => handleInputChange('closeEarlyWhenProfitable', e.target.checked)}
disabled={!formData.maxPositionTimeHours}
/>
</label>
</div>
</div>
{/* Advanced Parameters Dropdown */}
<div className="divider">
<button
type="button"
className="btn btn-outline btn-sm normal-case"
onClick={() => setShowAdvancedParams(!showAdvancedParams)}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Advanced Parameters
<svg
className={`w-4 h-4 ml-2 transition-transform duration-200 ${showAdvancedParams ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
{showAdvancedParams && (
<div className="space-y-4 border border-primary rounded-lg p-4 bg-base-100">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="form-control">
<label className="label">
<div className="flex items-center gap-2">
<span className="label-text">Cooldown Period (candles)</span>
<div className="tooltip tooltip-top" data-tip="Number of candles to wait before allowing another trade after closing a position. Prevents overtrading and allows market to settle">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<input
type="number"
className="input input-bordered"
value={formData.cooldownPeriod}
onChange={(e) => handleInputChange('cooldownPeriod', parseInt(e.target.value))}
min="1"
/>
</div>
<div className="form-control">
<label className="label">
<div className="flex items-center gap-2">
<span className="label-text">Max Loss Streak</span>
<div className="tooltip tooltip-top" data-tip="Maximum number of consecutive losing trades before stopping the bot. Set to 0 for no limit">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<input
type="number"
className="input input-bordered"
value={formData.maxLossStreak}
onChange={(e) => handleInputChange('maxLossStreak', parseInt(e.target.value))}
min="0"
/>
</div>
<div className="form-control">
<label className="label">
<div className="flex items-center gap-2">
<span className="label-text">Max Position Time (hours)</span>
<div className="tooltip tooltip-top" data-tip="Maximum time to hold a position before force closing. Leave empty to disable time-based position closure">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<input
type="number"
className="input input-bordered"
value={formData.maxPositionTimeHours || ''}
onChange={(e) => handleInputChange('maxPositionTimeHours', e.target.value ? parseFloat(e.target.value) : null)}
min="0.1"
step="0.1"
placeholder="Optional"
/>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Flip Only When In Profit</span>
<div className="tooltip tooltip-top" data-tip="If enabled, positions will only flip when current position is profitable. Helps avoid flipping during losing streaks">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.flipOnlyWhenInProfit}
onChange={(e) => handleInputChange('flipOnlyWhenInProfit', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Enable Position Flipping</span>
<div className="tooltip tooltip-top" data-tip="Allow the bot to flip between long and short positions based on signals. More aggressive trading strategy">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.flipPosition}
onChange={(e) => handleInputChange('flipPosition', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Close Early When Profitable</span>
<div className="tooltip tooltip-top" data-tip="If enabled, positions will close early when they become profitable. Conservative approach to lock in gains">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.closeEarlyWhenProfitable}
onChange={(e) => handleInputChange('closeEarlyWhenProfitable', e.target.checked)}
disabled={!formData.maxPositionTimeHours}
/>
</label>
</div>
</div>
</div>
)}
{/* Money Management Section */}
<div className="divider">Money Management</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">Use Custom Money Management</span>
<div className="flex items-center gap-2">
<span className="label-text">Use Custom Money Management</span>
<div className="tooltip tooltip-top" data-tip="Create custom risk management settings instead of using saved presets. Allows fine-tuning stop loss, take profit, and leverage">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
@@ -592,6 +717,84 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
</div>
)}
{/* Synth API Section */}
<div className="divider">Synth API Configuration</div>
<div className="grid grid-cols-1 gap-4">
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Enable Synth API</span>
<div className="tooltip tooltip-top" data-tip="Enable AI-powered probabilistic price forecasts and risk assessment using advanced machine learning models for enhanced trading decisions">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.useSynthApi}
onChange={(e) => handleSynthApiToggle(e.target.checked)}
/>
</label>
</div>
</div>
{/* Show sub-options only when Synth API is enabled */}
{formData.useSynthApi && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Use for Position Sizing</span>
<div className="tooltip tooltip-top" data-tip="Use Synth predictions for position sizing adjustments and risk assessment. Optimizes trade size based on confidence levels">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.useForPositionSizing}
onChange={(e) => handleInputChange('useForPositionSizing', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Use for Signal Filtering</span>
<div className="tooltip tooltip-top" data-tip="Use Synth predictions to filter trading signals. Only executes trades when AI confidence is high">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.useForSignalFiltering}
onChange={(e) => handleInputChange('useForSignalFiltering', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Use for Dynamic Stop Loss</span>
<div className="tooltip tooltip-top" data-tip="Use Synth predictions for dynamic stop-loss/take-profit adjustments. Adapts levels based on market conditions">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.useForDynamicStopLoss}
onChange={(e) => handleInputChange('useForDynamicStopLoss', e.target.checked)}
/>
</label>
</div>
</div>
)}
{/* Validation Messages */}
{formData.closeEarlyWhenProfitable && !formData.maxPositionTimeHours && (
<div className="alert alert-warning mt-4">

View File

@@ -4,18 +4,18 @@ import {type SubmitHandler, useForm} from 'react-hook-form'
import useApiUrlStore from '../../../app/store/apiStore'
import {
AccountClient,
BacktestClient,
BotType,
DataClient,
MoneyManagement,
MoneyManagementClient,
RunBacktestRequest,
Scenario,
ScenarioClient,
Ticker,
Timeframe,
TradingBotConfig,
AccountClient,
BacktestClient,
BotType,
DataClient,
MoneyManagement,
MoneyManagementClient,
RunBacktestRequest,
Scenario,
ScenarioClient,
Ticker,
Timeframe,
TradingBotConfigRequest,
} from '../../../generated/ManagingApi'
import type {BacktestModalProps, IBacktestsFormInput,} from '../../../global/type'
import {Loader, Slider} from '../../atoms'
@@ -42,7 +42,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
const [startDate, setStartDate] = useState<string>(defaultStartDateString);
const [endDate, setEndDate] = useState<string>(defaultEndDateString);
const { register, handleSubmit, setValue } = useForm<IBacktestsFormInput>({
const { register, handleSubmit, setValue, watch } = useForm<IBacktestsFormInput>({
defaultValues: {
startDate: defaultStartDateString,
endDate: defaultEndDateString,
@@ -51,9 +51,30 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
maxPositionTimeHours: null, // Default to null (disabled)
flipOnlyWhenInProfit: true, // Default to true
balance: 10000, // Default balance
closeEarlyWhenProfitable: false // Default to false
closeEarlyWhenProfitable: false, // Default to false
// Synth API defaults
useSynthApi: false,
useForPositionSizing: true,
useForSignalFiltering: true,
useForDynamicStopLoss: true
}
});
// Watch the useSynthApi value to conditionally show/hide sub-options
const useSynthApi = watch('useSynthApi');
// Reset sub-options when main Synth API toggle is turned off
useEffect(() => {
if (!useSynthApi) {
setValue('useForPositionSizing', false);
setValue('useForSignalFiltering', false);
setValue('useForDynamicStopLoss', false);
}
}, [useSynthApi, setValue]);
// State for advanced parameters dropdown
const [showAdvancedParams, setShowAdvancedParams] = useState(false);
const [selectedAccount, setSelectedAccount] = useState<string>('')
const [selectedTimeframe, setSelectedTimeframe] = useState<Timeframe>(Timeframe.OneHour)
const [selectedLoopQuantity, setLoopQuantity] = React.useState<number>(
@@ -127,36 +148,48 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
console.log(customScenario)
try {
// Create the TradingBotConfig
const tradingBotConfig: TradingBotConfig = {
// Create the TradingBotConfigRequest (note the Request suffix)
const tradingBotConfigRequest: TradingBotConfigRequest = {
accountName: form.accountName,
ticker: ticker as Ticker,
scenarioName: customScenario ? undefined : scenarioName,
scenario: customScenario,
scenario: customScenario ? {
name: customScenario.name || 'Custom Scenario',
indicators: customScenario.indicators?.map(indicator => ({
name: indicator.name || 'Unnamed Indicator',
type: indicator.type!,
signalType: indicator.signalType!,
minimumHistory: indicator.minimumHistory || 0,
period: indicator.period,
fastPeriods: indicator.fastPeriods,
slowPeriods: indicator.slowPeriods,
signalPeriods: indicator.signalPeriods,
multiplier: indicator.multiplier,
smoothPeriods: indicator.smoothPeriods,
stochPeriods: indicator.stochPeriods,
cyclePeriods: indicator.cyclePeriods
})) || [],
loopbackPeriod: customScenario.loopbackPeriod
} : undefined,
timeframe: form.timeframe,
botType: form.botType,
isForWatchingOnly: false, // Always false for backtests
isForBacktest: true, // Always true for backtests
cooldownPeriod: form.cooldownPeriod || 1,
maxLossStreak: form.maxLossStreak || 0,
maxPositionTimeHours: form.maxPositionTimeHours || null,
flipOnlyWhenInProfit: form.flipOnlyWhenInProfit ?? true,
flipPosition: form.botType === BotType.FlippingBot, // Set based on bot type
name: `Backtest-${customScenario ? customScenario.name : scenarioName}-${ticker}-${new Date().toISOString()}`,
botTradingBalance: form.balance,
moneyManagement: customMoneyManagement || moneyManagements?.find(m => m.name === selectedMoneyManagement) || moneyManagements?.[0] || {
name: 'placeholder',
leverage: 1,
stopLoss: 0.01,
takeProfit: 0.02,
timeframe: form.timeframe
},
closeEarlyWhenProfitable: form.closeEarlyWhenProfitable ?? false
closeEarlyWhenProfitable: form.closeEarlyWhenProfitable ?? false,
useSynthApi: form.useSynthApi ?? false,
useForPositionSizing: form.useForPositionSizing ?? true,
useForSignalFiltering: form.useForSignalFiltering ?? true,
useForDynamicStopLoss: form.useForDynamicStopLoss ?? true
};
// Create the RunBacktestRequest
const request: RunBacktestRequest = {
config: tradingBotConfig,
config: tradingBotConfigRequest, // Use the request object
startDate: new Date(form.startDate),
endDate: new Date(form.endDate),
balance: form.balance,
@@ -199,7 +232,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
function onMoneyManagementChange(e: any) {
if (e.target.value === 'custom') {
setShowCustomMoneyManagement(true)
setCustomMoneyManagement(e.target.value)
setCustomMoneyManagement(undefined) // Reset custom money management when switching to custom mode
} else {
setShowCustomMoneyManagement(false)
setCustomMoneyManagement(undefined)
@@ -210,7 +243,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
function onScenarioChange(e: any) {
if (e.target.value === 'custom') {
setShowCustomScenario(true)
setCustomScenario(e.target.value)
setCustomScenario(undefined) // Reset custom scenario when switching to custom mode
} else {
setShowCustomScenario(false)
setCustomScenario(undefined)
@@ -232,6 +265,11 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
useEffect(() => {
if (scenarios && scenarios.length > 0 && scenarios[0].name) {
setValue('scenarioName', scenarios[0].name);
setShowCustomScenario(false); // Hide custom scenario when scenarios are available
} else if (scenarios && scenarios.length === 0) {
// No scenarios available, automatically show custom scenario creation
setShowCustomScenario(true);
setValue('scenarioName', ''); // Clear any selected scenario
}
}, [scenarios, setValue]);
@@ -263,6 +301,12 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
if (moneyManagements && moneyManagements.length > 0){
setSelectedMoneyManagement(moneyManagements[0].name)
setCustomMoneyManagement(undefined)
setShowCustomMoneyManagement(false) // Hide custom money management when options are available
} else if (moneyManagements && moneyManagements.length === 0) {
// No money management options available, automatically show custom money management
setShowCustomMoneyManagement(true)
setSelectedMoneyManagement(undefined)
setCustomMoneyManagement(undefined)
}
}, [moneyManagements])
@@ -336,13 +380,19 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
},
})}
>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
{moneyManagements.length === 0 ? (
<option value="" disabled>No money management available - create a custom one below</option>
) : (
<>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</>
)}
<option key="custom" value="custom">
Custom
{moneyManagements.length === 0 ? 'Create Custom Money Management' : 'Custom'}
</option>
</select>
</FormInput>
@@ -405,18 +455,31 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
},
})}
>
<option value="" disabled>Select a scenario</option>
{scenarios.length === 0 ? (
<option value="" disabled>No scenarios available - create a custom one below</option>
) : (
<option value="" disabled>Select a scenario</option>
)}
{scenarios.map((item) => (
<option key={item.name || 'unnamed'} value={item.name || ''}>
{item.name || 'Unnamed Scenario'}
</option>
))}
<option key="custom" value="custom">
Custom
{scenarios.length === 0 ? 'Create Custom Scenario' : 'Custom'}
</option>
</select>
</FormInput>
{showCustomScenario && (
<div className="mt-6">
<CustomScenario
onCreateScenario={setCustomScenario}
showCustomScenario={showCustomScenario}
></CustomScenario>
</div>
)}
<FormInput label="Tickers" htmlFor="tickers">
<select
className="select select-bordered w-full"
@@ -433,17 +496,8 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
</FormInput>
</div>
{showCustomScenario && (
<div className="mt-6">
<CustomScenario
onCreateScenario={setCustomScenario}
showCustomScenario={showCustomScenario}
></CustomScenario>
</div>
)}
{/* Fourth Row: Balance & Cooldown Period */}
<div className="grid grid-cols-2 gap-4">
{/* Fourth Row: Balance */}
<div className="grid grid-cols-1 gap-4">
<FormInput label="Balance" htmlFor="balance">
<input
type="number"
@@ -455,125 +509,310 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
}}
/>
</FormInput>
<FormInput label="Cooldown Period (candles)" htmlFor="cooldownPeriod">
<input
type="number"
className="input input-bordered w-full"
min="1"
step="1"
{...register('cooldownPeriod', { valueAsNumber: true })}
/>
</FormInput>
</div>
{/* Fifth Row: Max Loss Streak & Max Position Time */}
<div className="grid grid-cols-2 gap-4">
<FormInput label="Max Loss Streak" htmlFor="maxLossStreak">
<input
type="number"
className="input input-bordered w-full"
min="0"
step="1"
{...register('maxLossStreak', { valueAsNumber: true })}
/>
</FormInput>
{/* Advanced Parameters Dropdown */}
<div className="divider">
<button
type="button"
className="btn btn-outline btn-sm normal-case"
onClick={() => setShowAdvancedParams(!showAdvancedParams)}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Advanced Parameters
<svg
className={`w-4 h-4 ml-2 transition-transform duration-200 ${showAdvancedParams ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<FormInput label="Max Position Time (hours)" htmlFor="maxPositionTimeHours">
<input
type="number"
className="input input-bordered w-full"
min="0"
step="0.5"
placeholder="Leave empty to disable"
{...register('maxPositionTimeHours', { valueAsNumber: true })}
/>
<div className="text-xs text-gray-500 mt-1">
Leave empty to disable time-based position closure
{showAdvancedParams && (
<div className="space-y-4 border border-primary rounded-lg p-4 bg-base-100">
{/* Cooldown Period & Dates */}
<div className="grid grid-cols-1 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Cooldown Period (candles)
<div className="tooltip tooltip-top" data-tip="Number of candles to wait before allowing another trade after closing a position. Prevents overtrading and allows market to settle">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="cooldownPeriod"
>
<input
type="number"
className="input input-bordered w-full"
min="1"
step="1"
{...register('cooldownPeriod', { valueAsNumber: true })}
/>
</FormInput>
</div>
</FormInput>
</div>
{/* Sixth Row: Flip Only When In Profit & Close Early When Profitable */}
<div className="grid grid-cols-2 gap-4">
<FormInput label="Flip Only When In Profit" htmlFor="flipOnlyWhenInProfit">
<input
type="checkbox"
className="toggle toggle-primary"
{...register('flipOnlyWhenInProfit')}
/>
<div className="text-xs text-gray-500 mt-1">
If enabled, positions will only flip when current position is profitable
{/* Start Date & End Date */}
<div className="grid grid-cols-2 gap-4">
<FormInput label="Start Date" htmlFor="startDate">
<input
type="date"
className="input input-bordered w-full"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
setValue('startDate', e.target.value);
}}
/>
</FormInput>
<FormInput label="End Date" htmlFor="endDate">
<input
type="date"
className="input input-bordered w-full"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
setValue('endDate', e.target.value);
}}
/>
</FormInput>
</div>
</FormInput>
<FormInput label="Close Early When Profitable" htmlFor="closeEarlyWhenProfitable">
<input
type="checkbox"
className="toggle toggle-primary"
{...register('closeEarlyWhenProfitable')}
/>
<div className="text-xs text-gray-500 mt-1">
If enabled, positions will close early when they become profitable
{/* Loop Slider (if enabled) */}
{showLoopSlider && (
<FormInput
label={
<div className="flex items-center gap-2">
Loop
<div className="tooltip tooltip-top" data-tip="Number of optimization loops to run for money management. Each loop uses the optimized parameters from the previous iteration">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="loop"
>
<Slider
id="loopSlider"
min="1"
max="10"
value={selectedLoopQuantity.toString()}
onChange={(e) => setLoopQuantity(Number(e.target.value))}
></Slider>
</FormInput>
)}
{/* Max Loss Streak & Max Position Time */}
<div className="grid grid-cols-2 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Max Loss Streak
<div className="tooltip tooltip-top" data-tip="Maximum number of consecutive losing trades before stopping the bot. Set to 0 for no limit">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="maxLossStreak"
>
<input
type="number"
className="input input-bordered w-full"
min="0"
step="1"
{...register('maxLossStreak', { valueAsNumber: true })}
/>
</FormInput>
<FormInput
label={
<div className="flex items-center gap-2">
Max Position Time (hours)
<div className="tooltip tooltip-top" data-tip="Maximum time to hold a position before force closing. Leave empty to disable time-based position closure">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="maxPositionTimeHours"
>
<input
type="number"
className="input input-bordered w-full"
min="0"
step="0.5"
placeholder="Leave empty to disable"
{...register('maxPositionTimeHours', { valueAsNumber: true })}
/>
</FormInput>
</div>
</FormInput>
</div>
{/* Seventh Row: Save */}
<div className="grid grid-cols-1 gap-4">
<FormInput label="Save" htmlFor="save">
<input
type="checkbox"
className="toggle toggle-primary"
{...register('save')}
/>
</FormInput>
</div>
{/* Flip Only When In Profit & Close Early When Profitable */}
<div className="grid grid-cols-2 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Flip Only When In Profit
<div className="tooltip tooltip-top" data-tip="If enabled, positions will only flip when current position is profitable. Helps avoid flipping during losing streaks">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="flipOnlyWhenInProfit"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('flipOnlyWhenInProfit')}
/>
</FormInput>
{/* Eighth Row: Start Date & End Date */}
<div className="grid grid-cols-2 gap-4">
<FormInput label="Start Date" htmlFor="startDate">
<input
type="date"
className="input input-bordered w-full"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
setValue('startDate', e.target.value);
}}
/>
</FormInput>
<FormInput
label={
<div className="flex items-center gap-2">
Close Early When Profitable
<div className="tooltip tooltip-top" data-tip="If enabled, positions will close early when they become profitable. Conservative approach to lock in gains">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="closeEarlyWhenProfitable"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('closeEarlyWhenProfitable')}
/>
</FormInput>
</div>
<FormInput label="End Date" htmlFor="endDate">
<input
type="date"
className="input input-bordered w-full"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
setValue('endDate', e.target.value);
}}
/>
</FormInput>
</div>
{/* Save Option */}
<div className="grid grid-cols-1 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Save Backtest Results
<div className="tooltip tooltip-top" data-tip="Save the backtest results to your account for future reference and analysis">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="save"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('save')}
/>
</FormInput>
</div>
{/* Loop Slider (if enabled) */}
{showLoopSlider && (
<FormInput label="Loop" htmlFor="loop">
<Slider
id="loopSlider"
min="1"
max="10"
value={selectedLoopQuantity.toString()}
onChange={(e) => setLoopQuantity(Number(e.target.value))}
></Slider>
</FormInput>
{/* Synth API Section */}
<div className="divider">Synth API Configuration</div>
<div className="grid grid-cols-1 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Enable Synth API
<div className="tooltip tooltip-top" data-tip="Enable AI-powered probabilistic price forecasts and risk assessment using advanced machine learning models for enhanced trading decisions">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="useSynthApi"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('useSynthApi')}
/>
</FormInput>
</div>
{/* Show sub-options only when Synth API is enabled */}
{useSynthApi && (
<div className="grid grid-cols-1 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Use for Position Sizing
<div className="tooltip tooltip-top" data-tip="Use Synth predictions for position sizing adjustments and risk assessment. Optimizes trade size based on confidence levels">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="useForPositionSizing"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('useForPositionSizing')}
/>
</FormInput>
<FormInput
label={
<div className="flex items-center gap-2">
Use for Signal Filtering
<div className="tooltip tooltip-top" data-tip="Use Synth predictions to filter trading signals. Only executes trades when AI confidence is high">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="useForSignalFiltering"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('useForSignalFiltering')}
/>
</FormInput>
<FormInput
label={
<div className="flex items-center gap-2">
Use for Dynamic Stop Loss
<div className="tooltip tooltip-top" data-tip="Use Synth predictions for dynamic stop-loss/take-profit adjustments. Adapts levels based on market conditions">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="useForDynamicStopLoss"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('useForDynamicStopLoss')}
/>
</FormInput>
</div>
)}
</div>
)}
</div>
<div className="modal-action">
<button type="submit" className="btn">
Run
<button
type="button"
className="btn btn-ghost"
onClick={closeModal}
>
Cancel
</button>
<button type="submit" className="btn btn-primary">
Run Backtest
</button>
</div>
</Modal>

View File

@@ -4,21 +4,16 @@ import {CardPosition, CardText} from '../../mollecules'
interface IBacktestRowDetailsProps {
backtest: Backtest;
optimizedMoneyManagement: {
stopLoss: number;
takeProfit: number;
};
}
const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
backtest,
optimizedMoneyManagement
backtest
}) => {
const {
candles,
positions,
walletBalances,
strategiesValues,
indicatorsValues,
signals,
statistics,
config
@@ -364,7 +359,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
candles={candles}
positions={positions}
walletBalances={walletBalances}
strategiesValues={strategiesValues}
indicatorsValues={indicatorsValues}
signals={signals}
></TradeChart>
</figure>

View File

@@ -1,4 +1,4 @@
import {ChevronDownIcon, ChevronRightIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid'
import {ChevronDownIcon, ChevronRightIcon, CogIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid'
import React, {useEffect, useState} from 'react'
import useApiUrlStore from '../../../app/store/apiStore'
@@ -6,7 +6,7 @@ import type {Backtest} from '../../../generated/ManagingApi'
import {BacktestClient} from '../../../generated/ManagingApi'
import type {IBacktestCards} from '../../../global/type'
import {CardText, SelectColumnFilter, Table} from '../../mollecules'
import BotConfigModal from '../../mollecules/BotConfigModal/BotConfigModal'
import {UnifiedTradingModal} from '../index'
import Toast from '../../mollecules/Toast/Toast'
import BacktestRowDetails from './backtestRowDetails'
@@ -32,6 +32,10 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [selectedBacktest, setSelectedBacktest] = useState<Backtest | null>(null)
// Backtest configuration modal state
const [showBacktestConfigModal, setShowBacktestConfigModal] = useState(false)
const [selectedBacktestForRerun, setSelectedBacktestForRerun] = useState<Backtest | null>(null)
const handleOpenBotConfigModal = (backtest: Backtest) => {
setSelectedBacktest(backtest)
setShowBotConfigModal(true)
@@ -42,6 +46,16 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
setSelectedBacktest(null)
}
const handleOpenBacktestConfigModal = (backtest: Backtest) => {
setSelectedBacktestForRerun(backtest)
setShowBacktestConfigModal(true)
}
const handleCloseBacktestConfigModal = () => {
setShowBacktestConfigModal(false)
setSelectedBacktestForRerun(null)
}
async function deleteBacktest(id: string) {
const t = new Toast('Deleting backtest')
const client = new BacktestClient({}, apiUrl)
@@ -213,6 +227,23 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
accessor: 'id',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Re-run backtest with same config">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBacktestConfigModal(cell.row.original as Backtest)}
>
<CogIcon className="text-info w-4"></CogIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'rerun',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>
@@ -429,18 +460,29 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
renderRowSubCompontent={({ row }: any) => (
<BacktestRowDetails
backtest={row.original}
optimizedMoneyManagement={optimizedMoneyManagement}
/>
)}
/>
{/* Bot Configuration Modal */}
{selectedBacktest && (
<BotConfigModal
<UnifiedTradingModal
showModal={showBotConfigModal}
mode="create"
mode="createBot"
backtest={selectedBacktest}
onClose={handleCloseBotConfigModal}
closeModal={handleCloseBotConfigModal}
/>
)}
{/* Backtest Configuration Modal */}
{selectedBacktestForRerun && (
<UnifiedTradingModal
showModal={showBacktestConfigModal}
mode="backtest"
backtest={selectedBacktestForRerun}
closeModal={handleCloseBacktestConfigModal}
setBacktests={setBacktests}
showLoopSlider={true}
/>
)}
</>

View File

@@ -16,11 +16,11 @@ import {useEffect, useRef, useState} from 'react'
import type {
Candle,
IndicatorsResultBase,
IndicatorType,
KeyValuePairOfDateTimeAndDecimal,
Position,
Signal,
StrategiesResultBase,
StrategyType,
} from '../../../../generated/ManagingApi'
import {PositionStatus, TradeDirection,} from '../../../../generated/ManagingApi'
import useTheme from '../../../../hooks/useTheme'
@@ -45,7 +45,7 @@ type ITradeChartProps = {
positions: Position[]
signals: Signal[]
walletBalances?: KeyValuePairOfDateTimeAndDecimal[] | null
strategiesValues?: { [key in keyof typeof StrategyType]?: StrategiesResultBase; } | null;
indicatorsValues?: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; } | null;
stream?: Candle | null
width: number
height: number
@@ -56,7 +56,7 @@ const TradeChart = ({
positions,
signals,
walletBalances,
strategiesValues,
indicatorsValues,
stream,
width,
height,
@@ -246,9 +246,6 @@ const TradeChart = ({
const data: CandlestickData[] = candles.map((c) => mapCandle(c))
let diff = 0; // Default to 0 if there's not enough data to calculate the difference
console.log(data)
console.log(data.length)
if (data.length > 3) {
diff =
(data[data.length - 1].time as number) -
@@ -308,7 +305,7 @@ const TradeChart = ({
}
// Price panel
if (strategiesValues?.EmaTrend != null || strategiesValues?.EmaCross != null)
if (indicatorsValues?.EmaTrend != null || indicatorsValues?.EmaCross != null)
{
const emaSeries = chart.current.addLineSeries({
color: theme.secondary,
@@ -323,7 +320,7 @@ const TradeChart = ({
title: 'EMA',
})
const ema = strategiesValues.EmaTrend?.ema ?? strategiesValues.EmaCross?.ema
const ema = indicatorsValues.EmaTrend?.ema ?? indicatorsValues.EmaCross?.ema
const emaData = ema?.map((w) => {
return {
@@ -339,7 +336,7 @@ const TradeChart = ({
}
}
if (strategiesValues?.SuperTrend != null) {
if (indicatorsValues?.SuperTrend != null) {
const superTrendSeries = chart.current.addLineSeries({
color: theme.info,
lineWidth: 1,
@@ -351,7 +348,7 @@ const TradeChart = ({
})
const superTrend = strategiesValues.SuperTrend.superTrend?.map((w) => {
const superTrend = indicatorsValues.SuperTrend.superTrend?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.superTrend,
@@ -361,6 +358,46 @@ const TradeChart = ({
superTrendSeries.setData(superTrend)
}
// Display chandeliers exits
if (indicatorsValues?.ChandelierExit != null) {
const chandelierExitsLongsSeries = chart.current.addLineSeries({
color: theme.info,
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
title: 'Chandelier Long',
pane: 0,
})
const chandelierExitsLongs = indicatorsValues.ChandelierExit.chandelierLong?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.chandelierExit,
}
})
// @ts-ignore
chandelierExitsLongsSeries.setData(chandelierExitsLongs)
const chandelierExitsShortsSeries = chart.current.addLineSeries({
color: theme.error,
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
title: 'Chandelier Short',
pane: 0,
})
const chandelierExitsShorts = indicatorsValues.ChandelierExit.chandelierShort?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.chandelierExit,
}
})
// @ts-ignore
chandelierExitsShortsSeries.setData(chandelierExitsShorts)
}
if (markers.length > 0) {
series1.current.setMarkers(markers)
}
@@ -369,14 +406,14 @@ const TradeChart = ({
var paneCount = 1
if (strategiesValues?.RsiDivergence != null || strategiesValues?.RsiDivergenceConfirm != null)
if (indicatorsValues?.RsiDivergence != null || indicatorsValues?.RsiDivergenceConfirm != null)
{
const rsiSeries = chart.current.addLineSeries({
pane: paneCount,
title: 'RSI',
})
const rsi = strategiesValues.RsiDivergence?.rsi ?? strategiesValues.RsiDivergenceConfirm?.rsi
const rsi = indicatorsValues.RsiDivergence?.rsi ?? indicatorsValues.RsiDivergenceConfirm?.rsi
const rsiData = rsi?.map((w) => {
return {
@@ -397,7 +434,7 @@ const TradeChart = ({
paneCount++
}
if (strategiesValues?.Stc != null) {
if (indicatorsValues?.Stc != null) {
const stcSeries = chart.current.addBaselineSeries({
pane: paneCount,
baseValue: {price: 50, type: 'price'},
@@ -408,7 +445,7 @@ const TradeChart = ({
stcSeries.createPriceLine(buildLine(theme.error, 25, 'low'))
stcSeries.createPriceLine(buildLine(theme.info, 75, 'high'))
const stcData = strategiesValues?.Stc.stc?.map((w) => {
const stcData = indicatorsValues?.Stc.stc?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.stc,
@@ -430,7 +467,42 @@ const TradeChart = ({
paneCount++
}
if (strategiesValues?.MacdCross != null) {
if (indicatorsValues?.LaggingStc != null) {
const laggingStcSeries = chart.current.addBaselineSeries({
pane: paneCount,
baseValue: {price: 50, type: 'price'},
title: 'Lagging STC',
})
laggingStcSeries.createPriceLine(buildLine(theme.error, 25, 'low'))
laggingStcSeries.createPriceLine(buildLine(theme.info, 75, 'high'))
const stcData = indicatorsValues?.LaggingStc.stc?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.stc,
}
})
// @ts-ignore
laggingStcSeries.setData(stcData)
laggingStcSeries.applyOptions({
...baselineOptions,
priceLineVisible: true,
priceFormat: {
minMove: 1,
precision: 1,
type: 'price',
},
crosshairMarkerVisible: true,
})
paneCount++
}
console.log(indicatorsValues)
if (indicatorsValues?.MacdCross != null) {
console.log(indicatorsValues.MacdCross)
const histogramSeries = chart.current.addHistogramSeries({
color: theme.accent,
title: 'MACD',
@@ -441,7 +513,7 @@ const TradeChart = ({
}
})
const macd = strategiesValues.MacdCross.macd?.map((w) => {
const macd = indicatorsValues.MacdCross.macd?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.histogram,
@@ -472,7 +544,7 @@ const TradeChart = ({
crosshairMarkerVisible: true,
})
const macdData = strategiesValues.MacdCross.macd?.map((w) => {
const macdData = indicatorsValues.MacdCross.macd?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.macd,
@@ -497,7 +569,7 @@ const TradeChart = ({
},
})
const signalData = strategiesValues.MacdCross.macd?.map((w) => {
const signalData = indicatorsValues.MacdCross.macd?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.signal,
@@ -510,7 +582,7 @@ const TradeChart = ({
paneCount++
}
if (strategiesValues?.StochRsiTrend){
if (indicatorsValues?.StochRsiTrend){
const stochRsiSeries = chart.current.addLineSeries({
...baselineOptions,
priceLineVisible: false,
@@ -518,7 +590,7 @@ const TradeChart = ({
pane: paneCount,
})
const stochRsi = strategiesValues.StochRsiTrend.stochRsi?.map((w) => {
const stochRsi = indicatorsValues.StochRsiTrend.stochRsi?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.stochRsi,
@@ -529,7 +601,7 @@ const TradeChart = ({
paneCount++
}
if (strategiesValues?.StDev != null) {
if (indicatorsValues?.StDev != null) {
const stDevSeries = chart.current.addLineSeries({
color: theme.primary,
lineWidth: 1,
@@ -539,7 +611,7 @@ const TradeChart = ({
pane: paneCount,
})
const stDev = strategiesValues.StDev.stdDev?.map((w) => {
const stDev = indicatorsValues.StDev.stdDev?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.stdDev,
@@ -557,7 +629,7 @@ const TradeChart = ({
crosshairMarkerVisible: true,
})
const zScore = strategiesValues.StDev.stdDev?.map((w) => {
const zScore = indicatorsValues.StDev.stdDev?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.zScore,
@@ -569,6 +641,47 @@ const TradeChart = ({
paneCount++
}
// Display dual EMA crossover
if (indicatorsValues?.DualEmaCross != null) {
const fastEmaSeries = chart.current.addLineSeries({
color: theme.info,
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
title: 'Fast EMA',
pane: paneCount,
})
const fastEma = indicatorsValues.DualEmaCross.fastEma?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.ema,
}
})
// @ts-ignore
fastEmaSeries.setData(fastEma)
const slowEmaSeries = chart.current.addLineSeries({
color: theme.primary,
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
title: 'Slow EMA',
pane: paneCount,
})
const slowEma = indicatorsValues.DualEmaCross.slowEma?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.ema,
}
})
// @ts-ignore
slowEmaSeries.setData(slowEma)
paneCount++
}
if (walletBalances != null && walletBalances.length > 0) {
const walletSeries = chart.current.addBaselineSeries({
baseValue: {price: walletBalances[0].value, type: 'price'},

View File

@@ -0,0 +1,124 @@
# UnifiedTradingModal
A unified modal component that replaces both `BacktestModal` and `BotConfigModal`. This component handles three different modes:
- **backtest**: Run backtests with multiple tickers
- **createBot**: Create a new trading bot (optionally from a backtest)
- **updateBot**: Update an existing bot's configuration
## Features
### ✅ **Unified Interface**
- Single component for all trading configuration needs
- Mode-specific form fields and validation
- Consistent UI/UX across all use cases
### ✅ **Advanced Configuration**
- **Advanced Parameters**: Collapsible section with cooldown periods, position limits, trading options
- **Risk Management**: Complete `RiskManagement` configuration with preset levels (Conservative, Moderate, Aggressive)
- **Synth API Integration**: AI-powered probabilistic forecasts and risk assessment
### ✅ **Smart Defaults**
- Context-aware initialization based on mode
- Automatic data loading and form population
- Preset risk management configurations
## Usage Examples
### 1. Backtest Mode
```tsx
import { UnifiedTradingModal } from '../../components/organism'
<UnifiedTradingModal
showModal={showModal}
closeModal={() => setShowModal(false)}
mode="backtest"
setBacktests={setBacktests}
showLoopSlider={true} // Optional: enable loop optimization
/>
```
### 2. Create Bot Mode
```tsx
<UnifiedTradingModal
showModal={showModal}
closeModal={() => setShowModal(false)}
mode="createBot"
/>
```
### 3. Create Bot from Backtest
```tsx
<UnifiedTradingModal
showModal={showModal}
closeModal={() => setShowModal(false)}
mode="createBot"
backtest={selectedBacktest} // Initialize from backtest
/>
```
### 4. Update Bot Mode
```tsx
<UnifiedTradingModal
showModal={showModal}
closeModal={() => setShowModal(false)}
mode="updateBot"
existingBot={{
identifier: bot.identifier,
config: bot.config
}}
/>
```
## Migration Guide
### From BacktestModal
```tsx
// Old
<BacktestModal
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktests}
showLoopSlider={true}
/>
// New
<UnifiedTradingModal
mode="backtest"
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktests}
showLoopSlider={true}
/>
```
### From BotConfigModal
```tsx
// Old
<BotConfigModal
showModal={showModal}
mode="create"
backtest={backtest}
onClose={closeModal}
/>
// New
<UnifiedTradingModal
mode="createBot"
showModal={showModal}
closeModal={closeModal}
backtest={backtest}
/>
```
## Risk Management Features
The component includes a comprehensive Risk Management section with:
- **Preset Levels**: Conservative, Moderate, Aggressive configurations
- **Kelly Criterion**: Position sizing based on edge and odds
- **Expected Utility**: Risk-adjusted decision making
- **Probability Thresholds**: Adverse and favorable signal validation
- **Liquidation Risk**: Maximum acceptable liquidation probability
All risk management parameters use the official `RiskManagement` type from the API and integrate seamlessly with the backend trading logic.

View File

@@ -0,0 +1 @@
export { default } from './UnifiedTradingModal'

View File

@@ -2,8 +2,12 @@ export { default as TradeChart } from './Trading/TradeChart/TradeChart'
export { default as CardPositionItem } from './Trading/CardPositionItem'
export { default as ActiveBots } from './ActiveBots/ActiveBots'
export { default as BacktestCards } from './Backtest/backtestCards'
export { default as BacktestModal } from './Backtest/backtestModal'
export { default as BacktestTable } from './Backtest/backtestTable'
// @deprecated - Use UnifiedTradingModal instead
export { default as BacktestModal } from './Backtest/backtestModal'
export { default as UnifiedTradingModal } from './UnifiedTradingModal'
export { default as CustomMoneyManagement } from './CustomMoneyManagement/CustomMoneyManagement'
export { default as CustomScenario } from './CustomScenario/CustomScenario'
export { default as SpotLightBadge } from './SpotLightBadge/SpotLightBadge'
export { default as StatusBadge } from './StatusBadge/StatusBadge'
export { default as PositionsList } from './Positions/PositionList'

View File

@@ -337,6 +337,45 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<FileResponse>(null as any);
}
backtest_Map(moneyManagementRequest: MoneyManagementRequest): Promise<MoneyManagement> {
let url_ = this.baseUrl + "/Backtest";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(moneyManagementRequest);
let options_: RequestInit = {
body: content_,
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBacktest_Map(_response);
});
}
protected processBacktest_Map(response: Response): Promise<MoneyManagement> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as MoneyManagement;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<MoneyManagement>(null as any);
}
backtest_Backtest(id: string): Promise<Backtest> {
let url_ = this.baseUrl + "/Backtest/{id}";
if (id === undefined || id === null)
@@ -726,6 +765,45 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<TradingBotResponse[]>(null as any);
}
bot_Map(moneyManagementRequest: MoneyManagementRequest): Promise<MoneyManagement> {
let url_ = this.baseUrl + "/Bot";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(moneyManagementRequest);
let options_: RequestInit = {
body: content_,
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBot_Map(_response);
});
}
protected processBot_Map(response: Response): Promise<MoneyManagement> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as MoneyManagement;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<MoneyManagement>(null as any);
}
bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise<Position> {
let url_ = this.baseUrl + "/Bot/OpenPosition";
url_ = url_.replace(/[?&]$/, "");
@@ -1432,7 +1510,7 @@ export class ScenarioClient extends AuthorizedApiBase {
this.baseUrl = baseUrl ?? "http://localhost:5000";
}
scenario_GetScenarios(): Promise<Scenario[]> {
scenario_GetScenarios(): Promise<ScenarioViewModel[]> {
let url_ = this.baseUrl + "/Scenario";
url_ = url_.replace(/[?&]$/, "");
@@ -1450,13 +1528,13 @@ export class ScenarioClient extends AuthorizedApiBase {
});
}
protected processScenario_GetScenarios(response: Response): Promise<Scenario[]> {
protected processScenario_GetScenarios(response: Response): Promise<ScenarioViewModel[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Scenario[];
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ScenarioViewModel[];
return result200;
});
} else if (status !== 200 && status !== 204) {
@@ -1464,10 +1542,10 @@ export class ScenarioClient extends AuthorizedApiBase {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Scenario[]>(null as any);
return Promise.resolve<ScenarioViewModel[]>(null as any);
}
scenario_CreateScenario(name: string | null | undefined, loopbackPeriod: number | null | undefined, strategies: string[]): Promise<Scenario> {
scenario_CreateScenario(name: string | null | undefined, loopbackPeriod: number | null | undefined, strategies: string[]): Promise<ScenarioViewModel> {
let url_ = this.baseUrl + "/Scenario?";
if (name !== undefined && name !== null)
url_ += "name=" + encodeURIComponent("" + name) + "&";
@@ -1493,13 +1571,13 @@ export class ScenarioClient extends AuthorizedApiBase {
});
}
protected processScenario_CreateScenario(response: Response): Promise<Scenario> {
protected processScenario_CreateScenario(response: Response): Promise<ScenarioViewModel> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Scenario;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ScenarioViewModel;
return result200;
});
} else if (status !== 200 && status !== 204) {
@@ -1507,7 +1585,7 @@ export class ScenarioClient extends AuthorizedApiBase {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Scenario>(null as any);
return Promise.resolve<ScenarioViewModel>(null as any);
}
scenario_DeleteScenario(name: string | null | undefined): Promise<FileResponse> {
@@ -1600,7 +1678,7 @@ export class ScenarioClient extends AuthorizedApiBase {
return Promise.resolve<FileResponse>(null as any);
}
scenario_GetIndicators(): Promise<Indicator[]> {
scenario_GetIndicators(): Promise<IndicatorViewModel[]> {
let url_ = this.baseUrl + "/Scenario/indicator";
url_ = url_.replace(/[?&]$/, "");
@@ -1618,13 +1696,13 @@ export class ScenarioClient extends AuthorizedApiBase {
});
}
protected processScenario_GetIndicators(response: Response): Promise<Indicator[]> {
protected processScenario_GetIndicators(response: Response): Promise<IndicatorViewModel[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Indicator[];
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as IndicatorViewModel[];
return result200;
});
} else if (status !== 200 && status !== 204) {
@@ -1632,10 +1710,10 @@ export class ScenarioClient extends AuthorizedApiBase {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Indicator[]>(null as any);
return Promise.resolve<IndicatorViewModel[]>(null as any);
}
scenario_CreateIndicator(indicatorType: IndicatorType | undefined, name: string | null | undefined, period: number | null | undefined, fastPeriods: number | null | undefined, slowPeriods: number | null | undefined, signalPeriods: number | null | undefined, multiplier: number | null | undefined, stochPeriods: number | null | undefined, smoothPeriods: number | null | undefined, cyclePeriods: number | null | undefined): Promise<Indicator> {
scenario_CreateIndicator(indicatorType: IndicatorType | undefined, name: string | null | undefined, period: number | null | undefined, fastPeriods: number | null | undefined, slowPeriods: number | null | undefined, signalPeriods: number | null | undefined, multiplier: number | null | undefined, stochPeriods: number | null | undefined, smoothPeriods: number | null | undefined, cyclePeriods: number | null | undefined): Promise<IndicatorViewModel> {
let url_ = this.baseUrl + "/Scenario/indicator?";
if (indicatorType === null)
throw new Error("The parameter 'indicatorType' cannot be null.");
@@ -1675,13 +1753,13 @@ export class ScenarioClient extends AuthorizedApiBase {
});
}
protected processScenario_CreateIndicator(response: Response): Promise<Indicator> {
protected processScenario_CreateIndicator(response: Response): Promise<IndicatorViewModel> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Indicator;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as IndicatorViewModel;
return result200;
});
} else if (status !== 200 && status !== 204) {
@@ -1689,7 +1767,7 @@ export class ScenarioClient extends AuthorizedApiBase {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Indicator>(null as any);
return Promise.resolve<IndicatorViewModel>(null as any);
}
scenario_DeleteIndicator(name: string | null | undefined): Promise<FileResponse> {
@@ -2779,7 +2857,7 @@ export interface Backtest {
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
optimizedMoneyManagement: MoneyManagement;
user: User;
strategiesValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
score: number;
}
@@ -2796,11 +2874,16 @@ export interface TradingBotConfig {
maxLossStreak: number;
flipPosition: boolean;
name: string;
riskManagement?: RiskManagement | null;
scenario?: Scenario | null;
scenarioName?: string | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
flipOnlyWhenInProfit: boolean;
useSynthApi?: boolean;
useForPositionSizing?: boolean;
useForSignalFiltering?: boolean;
useForDynamicStopLoss?: boolean;
}
export interface MoneyManagement {
@@ -2937,6 +3020,29 @@ export enum BotType {
FlippingBot = "FlippingBot",
}
export interface RiskManagement {
adverseProbabilityThreshold: number;
favorableProbabilityThreshold: number;
riskAversion: number;
kellyMinimumThreshold: number;
kellyMaximumCap: number;
maxLiquidationProbability: number;
signalValidationTimeHorizonHours: number;
positionMonitoringTimeHorizonHours: number;
positionWarningThreshold: number;
positionAutoCloseThreshold: number;
kellyFractionalMultiplier: number;
riskTolerance: RiskToleranceLevel;
useExpectedUtility: boolean;
useKellyCriterion: boolean;
}
export enum RiskToleranceLevel {
Conservative = "Conservative",
Moderate = "Moderate",
Aggressive = "Aggressive",
}
export interface Scenario {
name?: string | null;
indicators?: Indicator[] | null;
@@ -3086,6 +3192,7 @@ export interface Signal extends ValueObject {
indicatorType: IndicatorType;
signalType: SignalType;
user?: User | null;
indicatorName: string;
}
export enum SignalStatus {
@@ -3226,19 +3333,68 @@ export interface SuperTrendResult extends ResultBase {
}
export interface RunBacktestRequest {
config?: TradingBotConfig | null;
config?: TradingBotConfigRequest | null;
startDate?: Date;
endDate?: Date;
balance?: number;
watchOnly?: boolean;
save?: boolean;
}
export interface TradingBotConfigRequest {
accountName: string;
ticker: Ticker;
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botType: BotType;
name: string;
cooldownPeriod: number;
maxLossStreak: number;
scenario?: ScenarioRequest | null;
scenarioName?: string | null;
moneyManagementName?: string | null;
moneyManagement?: MoneyManagement | null;
moneyManagement?: MoneyManagementRequest | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
flipOnlyWhenInProfit?: boolean;
useSynthApi?: boolean;
useForPositionSizing?: boolean;
useForSignalFiltering?: boolean;
useForDynamicStopLoss?: boolean;
}
export interface ScenarioRequest {
name: string;
indicators: IndicatorRequest[];
loopbackPeriod?: number | null;
}
export interface IndicatorRequest {
name: string;
type: IndicatorType;
signalType: SignalType;
minimumHistory?: number;
period?: number | null;
fastPeriods?: number | null;
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
}
export interface MoneyManagementRequest {
name: string;
timeframe: Timeframe;
stopLoss: number;
takeProfit: number;
leverage: number;
}
export interface StartBotRequest {
config?: TradingBotConfig | null;
moneyManagementName?: string | null;
config?: TradingBotConfigRequest | null;
}
export interface TradingBotResponse {
@@ -3265,8 +3421,9 @@ export interface ClosePositionRequest {
export interface UpdateBotConfigRequest {
identifier: string;
config: TradingBotConfig;
config: TradingBotConfigRequest;
moneyManagementName?: string | null;
moneyManagement?: MoneyManagement | null;
}
export interface TickerInfos {
@@ -3372,6 +3529,29 @@ export interface BestAgentsResponse {
totalPages?: number;
}
export interface ScenarioViewModel {
name: string;
indicators: IndicatorViewModel[];
loopbackPeriod?: number | null;
userName: string;
}
export interface IndicatorViewModel {
name: string;
type: IndicatorType;
signalType: SignalType;
minimumHistory: number;
period?: number | null;
fastPeriods?: number | null;
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
userName: string;
}
export enum RiskLevel {
Low = "Low",
Medium = "Medium",

View File

@@ -0,0 +1,887 @@
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
/* tslint:disable */
/* eslint-disable */
// ReSharper disable InconsistentNaming
export interface Account {
name: string;
exchange: TradingExchanges;
type: AccountType;
key?: string | null;
secret?: string | null;
user?: User | null;
balances?: Balance[] | null;
isPrivyWallet?: boolean;
}
export enum TradingExchanges {
Binance = "Binance",
Kraken = "Kraken",
Ftx = "Ftx",
Evm = "Evm",
GmxV2 = "GmxV2",
}
export enum AccountType {
Cex = "Cex",
Trader = "Trader",
Watch = "Watch",
Auth = "Auth",
Privy = "Privy",
}
export interface User {
name?: string | null;
accounts?: Account[] | null;
agentName?: string | null;
avatarUrl?: string | null;
telegramChannel?: string | null;
}
export interface Balance {
tokenImage?: string | null;
tokenName?: string | null;
amount?: number;
price?: number;
value?: number;
tokenAdress?: string | null;
chain?: Chain | null;
}
export interface Chain {
id?: string | null;
rpcUrl?: string | null;
name?: string | null;
chainId?: number;
}
export interface GmxClaimableSummary {
claimableFundingFees?: FundingFeesData | null;
claimableUiFees?: UiFeesData | null;
rebateStats?: RebateStatsData | null;
}
export interface FundingFeesData {
totalUsdc?: number;
}
export interface UiFeesData {
totalUsdc?: number;
}
export interface RebateStatsData {
totalRebateUsdc?: number;
discountUsdc?: number;
rebateFactor?: number;
discountFactor?: number;
}
export interface Backtest {
id: string;
finalPnl: number;
winRate: number;
growthPercentage: number;
hodlPercentage: number;
config: TradingBotConfig;
positions: Position[];
signals: Signal[];
candles: Candle[];
startDate: Date;
endDate: Date;
statistics: PerformanceMetrics;
fees: number;
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
optimizedMoneyManagement: MoneyManagement;
user: User;
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
score: number;
}
export interface TradingBotConfig {
accountName: string;
moneyManagement: MoneyManagement;
ticker: Ticker;
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botType: BotType;
isForBacktest: boolean;
cooldownPeriod: number;
maxLossStreak: number;
flipPosition: boolean;
name: string;
riskManagement?: RiskManagement | null;
scenario?: Scenario | null;
scenarioName?: string | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
flipOnlyWhenInProfit: boolean;
useSynthApi?: boolean;
useForPositionSizing?: boolean;
useForSignalFiltering?: boolean;
useForDynamicStopLoss?: boolean;
}
export interface MoneyManagement {
name: string;
timeframe: Timeframe;
stopLoss: number;
takeProfit: number;
leverage: number;
user?: User | null;
}
export enum Timeframe {
FiveMinutes = "FiveMinutes",
FifteenMinutes = "FifteenMinutes",
ThirtyMinutes = "ThirtyMinutes",
OneHour = "OneHour",
FourHour = "FourHour",
OneDay = "OneDay",
OneMinute = "OneMinute",
}
export enum Ticker {
AAVE = "AAVE",
ADA = "ADA",
APE = "APE",
ALGO = "ALGO",
ARB = "ARB",
ATOM = "ATOM",
AVAX = "AVAX",
BNB = "BNB",
BTC = "BTC",
BAL = "BAL",
CHZ = "CHZ",
COMP = "COMP",
CRO = "CRO",
CRV = "CRV",
DOGE = "DOGE",
DOT = "DOT",
DYDX = "DYDX",
ENS = "ENS",
ETC = "ETC",
ETH = "ETH",
FIL = "FIL",
FLM = "FLM",
FTM = "FTM",
GALA = "GALA",
GMX = "GMX",
GRT = "GRT",
IMX = "IMX",
JASMY = "JASMY",
KSM = "KSM",
LDO = "LDO",
LINK = "LINK",
LRC = "LRC",
LTC = "LTC",
MANA = "MANA",
MATIC = "MATIC",
MKR = "MKR",
NEAR = "NEAR",
OP = "OP",
PEPE = "PEPE",
QTUM = "QTUM",
REN = "REN",
ROSE = "ROSE",
RSR = "RSR",
RUNE = "RUNE",
SAND = "SAND",
SOL = "SOL",
SRM = "SRM",
SUSHI = "SUSHI",
THETA = "THETA",
UNI = "UNI",
USDC = "USDC",
USDT = "USDT",
WIF = "WIF",
XMR = "XMR",
XRP = "XRP",
XTZ = "XTZ",
SHIB = "SHIB",
STX = "STX",
ORDI = "ORDI",
APT = "APT",
BOME = "BOME",
MEME = "MEME",
FLOKI = "FLOKI",
MEW = "MEW",
TAO = "TAO",
BONK = "BONK",
WLD = "WLD",
TBTC = "tBTC",
WBTC_b = "WBTC_b",
EIGEN = "EIGEN",
SUI = "SUI",
SEI = "SEI",
USDC_e = "USDC_e",
DAI = "DAI",
TIA = "TIA",
TRX = "TRX",
TON = "TON",
PENDLE = "PENDLE",
WstETH = "wstETH",
USDe = "USDe",
SATS = "SATS",
POL = "POL",
XLM = "XLM",
BCH = "BCH",
ICP = "ICP",
RENDER = "RENDER",
INJ = "INJ",
TRUMP = "TRUMP",
MELANIA = "MELANIA",
ENA = "ENA",
FARTCOIN = "FARTCOIN",
AI16Z = "AI16Z",
ANIME = "ANIME",
BERA = "BERA",
VIRTUAL = "VIRTUAL",
PENGU = "PENGU",
ONDO = "ONDO",
FET = "FET",
AIXBT = "AIXBT",
CAKE = "CAKE",
S = "S",
JUP = "JUP",
HYPE = "HYPE",
OM = "OM",
DOLO = "DOLO",
Unknown = "Unknown",
}
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
}
export interface RiskManagement {
adverseProbabilityThreshold: number;
favorableProbabilityThreshold: number;
riskAversion: number;
kellyMinimumThreshold: number;
kellyMaximumCap: number;
maxLiquidationProbability: number;
signalValidationTimeHorizonHours: number;
positionMonitoringTimeHorizonHours: number;
positionWarningThreshold: number;
positionAutoCloseThreshold: number;
kellyFractionalMultiplier: number;
riskTolerance: RiskToleranceLevel;
useExpectedUtility: boolean;
useKellyCriterion: boolean;
}
export enum RiskToleranceLevel {
Conservative = "Conservative",
Moderate = "Moderate",
Aggressive = "Aggressive",
}
export interface Scenario {
name?: string | null;
indicators?: Indicator[] | null;
loopbackPeriod?: number | null;
user?: User | null;
}
export interface Indicator {
name?: string | null;
type?: IndicatorType;
signalType?: SignalType;
minimumHistory?: number;
period?: number | null;
fastPeriods?: number | null;
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
user?: User | null;
}
export enum IndicatorType {
RsiDivergence = "RsiDivergence",
RsiDivergenceConfirm = "RsiDivergenceConfirm",
MacdCross = "MacdCross",
EmaCross = "EmaCross",
ThreeWhiteSoldiers = "ThreeWhiteSoldiers",
SuperTrend = "SuperTrend",
ChandelierExit = "ChandelierExit",
EmaTrend = "EmaTrend",
Composite = "Composite",
StochRsiTrend = "StochRsiTrend",
Stc = "Stc",
StDev = "StDev",
LaggingStc = "LaggingStc",
SuperTrendCrossEma = "SuperTrendCrossEma",
DualEmaCross = "DualEmaCross",
}
export enum SignalType {
Signal = "Signal",
Trend = "Trend",
Context = "Context",
}
export interface Position {
accountName: string;
date: Date;
originDirection: TradeDirection;
ticker: Ticker;
moneyManagement: MoneyManagement;
open: Trade;
stopLoss: Trade;
takeProfit1: Trade;
takeProfit2?: Trade | null;
profitAndLoss?: ProfitAndLoss | null;
status: PositionStatus;
signalIdentifier?: string | null;
identifier: string;
initiator: PositionInitiator;
user: User;
}
export enum TradeDirection {
None = "None",
Short = "Short",
Long = "Long",
}
export interface Trade {
fee?: number;
date: Date;
direction: TradeDirection;
status: TradeStatus;
tradeType: TradeType;
ticker: Ticker;
quantity: number;
price: number;
leverage?: number;
exchangeOrderId: string;
message?: string | null;
}
export enum TradeStatus {
PendingOpen = "PendingOpen",
Requested = "Requested",
Cancelled = "Cancelled",
Filled = "Filled",
}
export enum TradeType {
Limit = "Limit",
Market = "Market",
StopMarket = "StopMarket",
StopLimit = "StopLimit",
StopLoss = "StopLoss",
TakeProfit = "TakeProfit",
StopLossProfit = "StopLossProfit",
StopLossProfitLimit = "StopLossProfitLimit",
StopLossLimit = "StopLossLimit",
TakeProfitLimit = "TakeProfitLimit",
TrailingStop = "TrailingStop",
TrailingStopLimit = "TrailingStopLimit",
StopLossAndLimit = "StopLossAndLimit",
SettlePosition = "SettlePosition",
}
export interface ProfitAndLoss {
realized?: number;
net?: number;
averageOpenPrice?: number;
}
export enum PositionStatus {
New = "New",
Canceled = "Canceled",
Rejected = "Rejected",
Updating = "Updating",
PartiallyFilled = "PartiallyFilled",
Filled = "Filled",
Flipped = "Flipped",
Finished = "Finished",
}
export enum PositionInitiator {
PaperTrading = "PaperTrading",
Bot = "Bot",
User = "User",
CopyTrading = "CopyTrading",
}
export interface ValueObject {
}
export interface Signal extends ValueObject {
status: SignalStatus;
direction: TradeDirection;
confidence: Confidence;
timeframe: Timeframe;
date: Date;
candle: Candle;
identifier: string;
ticker: Ticker;
exchange: TradingExchanges;
indicatorType: IndicatorType;
signalType: SignalType;
user?: User | null;
indicatorName: string;
}
export enum SignalStatus {
WaitingForPosition = "WaitingForPosition",
PositionOpen = "PositionOpen",
Expired = "Expired",
}
export enum Confidence {
Low = "Low",
Medium = "Medium",
High = "High",
None = "None",
}
export interface Candle {
exchange: TradingExchanges;
ticker: string;
openTime: Date;
date: Date;
open: number;
close: number;
volume?: number;
high: number;
low: number;
baseVolume?: number;
quoteVolume?: number;
tradeCount?: number;
takerBuyBaseVolume?: number;
takerBuyQuoteVolume?: number;
timeframe: Timeframe;
}
export interface PerformanceMetrics {
count?: number;
sharpeRatio?: number;
maxDrawdown?: number;
maxDrawdownPc?: number;
maxDrawdownRecoveryTime?: string;
winningTrades?: number;
loosingTrades?: number;
totalPnL?: number;
}
export interface KeyValuePairOfDateTimeAndDecimal {
key?: Date;
value?: number;
}
export interface IndicatorsResultBase {
ema?: EmaResult[] | null;
fastEma?: EmaResult[] | null;
slowEma?: EmaResult[] | null;
macd?: MacdResult[] | null;
rsi?: RsiResult[] | null;
stoch?: StochResult[] | null;
stochRsi?: StochRsiResult[] | null;
bollingerBands?: BollingerBandsResult[] | null;
chandelierShort?: ChandelierResult[] | null;
stc?: StcResult[] | null;
stdDev?: StdDevResult[] | null;
superTrend?: SuperTrendResult[] | null;
chandelierLong?: ChandelierResult[] | null;
}
export interface ResultBase {
date?: Date;
}
export interface EmaResult extends ResultBase {
ema?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface MacdResult extends ResultBase {
macd?: number | null;
signal?: number | null;
histogram?: number | null;
fastEma?: number | null;
slowEma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface RsiResult extends ResultBase {
rsi?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */
export interface StochResult extends ResultBase {
oscillator?: number | null;
signal?: number | null;
percentJ?: number | null;
k?: number | null;
d?: number | null;
j?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StochRsiResult extends ResultBase {
stochRsi?: number | null;
signal?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface BollingerBandsResult extends ResultBase {
sma?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
percentB?: number | null;
zScore?: number | null;
width?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface ChandelierResult extends ResultBase {
chandelierExit?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StcResult extends ResultBase {
stc?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StdDevResult extends ResultBase {
stdDev?: number | null;
mean?: number | null;
zScore?: number | null;
stdDevSma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface SuperTrendResult extends ResultBase {
superTrend?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
}
export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null;
startDate?: Date;
endDate?: Date;
balance?: number;
watchOnly?: boolean;
save?: boolean;
}
export interface TradingBotConfigRequest {
accountName: string;
ticker: Ticker;
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botType: BotType;
name: string;
cooldownPeriod: number;
maxLossStreak: number;
scenario?: ScenarioRequest | null;
scenarioName?: string | null;
moneyManagementName?: string | null;
moneyManagement?: MoneyManagementRequest | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
flipOnlyWhenInProfit?: boolean;
useSynthApi?: boolean;
useForPositionSizing?: boolean;
useForSignalFiltering?: boolean;
useForDynamicStopLoss?: boolean;
}
export interface ScenarioRequest {
name: string;
indicators: IndicatorRequest[];
loopbackPeriod?: number | null;
}
export interface IndicatorRequest {
name: string;
type: IndicatorType;
signalType: SignalType;
minimumHistory?: number;
period?: number | null;
fastPeriods?: number | null;
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
}
export interface MoneyManagementRequest {
name: string;
timeframe: Timeframe;
stopLoss: number;
takeProfit: number;
leverage: number;
}
export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}
export interface TradingBotResponse {
status: string;
signals: Signal[];
positions: Position[];
candles: Candle[];
winRate: number;
profitAndLoss: number;
identifier: string;
agentName: string;
config: TradingBotConfig;
}
export interface OpenPositionManuallyRequest {
identifier?: string | null;
direction?: TradeDirection;
}
export interface ClosePositionRequest {
identifier?: string | null;
positionId?: string | null;
}
export interface UpdateBotConfigRequest {
identifier: string;
config: TradingBotConfigRequest;
moneyManagementName?: string | null;
moneyManagement?: MoneyManagement | null;
}
export interface TickerInfos {
ticker?: Ticker;
imageUrl?: string | null;
}
export interface SpotlightOverview {
spotlights: Spotlight[];
dateTime: Date;
identifier?: string;
scenarioCount?: number;
}
export interface Spotlight {
scenario: Scenario;
tickerSignals: TickerSignal[];
}
export interface TickerSignal {
ticker: Ticker;
fiveMinutes: Signal[];
fifteenMinutes: Signal[];
oneHour: Signal[];
fourHour: Signal[];
oneDay: Signal[];
}
export interface StrategiesStatisticsViewModel {
totalStrategiesRunning?: number;
changeInLast24Hours?: number;
}
export interface TopStrategiesViewModel {
topStrategies?: StrategyPerformance[] | null;
}
export interface StrategyPerformance {
strategyName?: string | null;
pnL?: number;
}
export interface UserStrategyDetailsViewModel {
name?: string | null;
state?: string | null;
pnL?: number;
roiPercentage?: number;
roiLast24H?: number;
runtime?: Date;
winRate?: number;
totalVolumeTraded?: number;
volumeLast24H?: number;
wins?: number;
losses?: number;
positions?: Position[] | null;
identifier?: string | null;
scenarioName?: string | null;
}
export interface PlatformSummaryViewModel {
totalAgents?: number;
totalActiveStrategies?: number;
totalPlatformPnL?: number;
totalPlatformVolume?: number;
totalPlatformVolumeLast24h?: number;
agentSummaries?: AgentSummaryViewModel[] | null;
timeFilter?: string | null;
}
export interface AgentSummaryViewModel {
agentName?: string | null;
totalPnL?: number;
pnLLast24h?: number;
totalROI?: number;
roiLast24h?: number;
wins?: number;
losses?: number;
averageWinRate?: number;
activeStrategiesCount?: number;
totalVolume?: number;
volumeLast24h?: number;
}
export interface AgentBalanceHistory {
agentName?: string | null;
agentBalances?: AgentBalance[] | null;
}
export interface AgentBalance {
agentName?: string | null;
totalValue?: number;
totalAccountUsdValue?: number;
botsAllocationUsdValue?: number;
pnL?: number;
time?: Date;
}
export interface BestAgentsResponse {
agents?: AgentBalanceHistory[] | null;
totalCount?: number;
currentPage?: number;
pageSize?: number;
totalPages?: number;
}
export interface ScenarioViewModel {
name: string;
indicators: IndicatorViewModel[];
loopbackPeriod?: number | null;
userName: string;
}
export interface IndicatorViewModel {
name: string;
type: IndicatorType;
signalType: SignalType;
minimumHistory: number;
period?: number | null;
fastPeriods?: number | null;
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
userName: string;
}
export enum RiskLevel {
Low = "Low",
Medium = "Medium",
High = "High",
Adaptive = "Adaptive",
}
export interface PrivyInitAddressResponse {
success?: boolean;
usdcHash?: string | null;
orderVaultHash?: string | null;
exchangeRouterHash?: string | null;
error?: string | null;
}
export interface LoginRequest {
name: string;
address: string;
signature: string;
message: string;
}
export interface Workflow {
name: string;
usage: WorkflowUsage;
flows: IFlow[];
description: string;
}
export enum WorkflowUsage {
Trading = "Trading",
Task = "Task",
}
export interface IFlow {
id: string;
name: string;
type: FlowType;
description: string;
acceptedInputs: FlowOutput[];
children?: IFlow[] | null;
parameters: FlowParameter[];
parentId?: string;
output?: string | null;
outputTypes: FlowOutput[];
}
export enum FlowType {
RsiDivergence = "RsiDivergence",
FeedTicker = "FeedTicker",
OpenPosition = "OpenPosition",
}
export enum FlowOutput {
Signal = "Signal",
Candles = "Candles",
Position = "Position",
MoneyManagement = "MoneyManagement",
}
export interface FlowParameter {
value?: any | null;
name?: string | null;
}
export interface SyntheticWorkflow {
name: string;
usage: WorkflowUsage;
description: string;
flows: SyntheticFlow[];
}
export interface SyntheticFlow {
id: string;
parentId?: string | null;
type: FlowType;
parameters: SyntheticFlowParameter[];
}
export interface SyntheticFlowParameter {
value: string;
name: string;
}
export interface FileResponse {
data: Blob;
status: number;
fileName?: string;
headers?: { [name: string]: any };
}

View File

@@ -0,0 +1,50 @@
// Import types from ManagingApi
import type {Backtest, RiskManagement, TradingBotConfig} from '../generated/ManagingApi'
// Import the existing IBacktestsFormInput from the correct file
import type {IBacktestsFormInput} from './type.tsx'
export interface IRiskManagementInput {
adverseProbabilityThreshold: number
favorableProbabilityThreshold: number
riskAversion: number
kellyMinimumThreshold: number
kellyMaximumCap: number
maxLiquidationProbability: number
signalValidationTimeHorizonHours: number
positionMonitoringTimeHorizonHours: number
positionWarningThreshold: number
positionAutoCloseThreshold: number
kellyFractionalMultiplier: number
riskTolerance: 'Conservative' | 'Moderate' | 'Aggressive'
useExpectedUtility: boolean
useKellyCriterion: boolean
}
export interface IUnifiedTradingConfigInput extends IBacktestsFormInput {
// Bot-specific fields
name?: string
isForWatchingOnly?: boolean
flipPosition?: boolean
// Risk Management fields
riskManagement?: RiskManagement
useCustomRiskManagement?: boolean
}
export interface UnifiedTradingModalProps {
showModal: boolean
closeModal: () => void
mode: 'backtest' | 'createBot' | 'updateBot'
showLoopSlider?: boolean
// For backtests
setBacktests?: React.Dispatch<React.SetStateAction<Backtest[]>>
// For bot creation/update
backtest?: Backtest // Backtest object when creating from backtest
existingBot?: {
identifier: string
config: TradingBotConfig // TradingBotConfig from API
}
}

View File

@@ -10,11 +10,11 @@ import type {
FlowOutput,
FlowType,
IFlow,
Indicator,
IndicatorViewModel,
MoneyManagement,
Position,
RiskLevel,
Scenario,
ScenarioViewModel,
Signal,
Ticker,
Timeframe,
@@ -112,6 +112,11 @@ export type IBacktestsFormInput = {
maxPositionTimeHours?: number | null
flipOnlyWhenInProfit?: boolean
closeEarlyWhenProfitable?: boolean
// Synth API fields
useSynthApi?: boolean
useForPositionSizing?: boolean
useForSignalFiltering?: boolean
useForDynamicStopLoss?: boolean
}
export type IBacktestCards = {
@@ -123,7 +128,7 @@ export type IBacktestCards = {
export type IFormInput = {
children: React.ReactNode
htmlFor: string
label: string
label: React.ReactNode
inline?: boolean
}
@@ -177,9 +182,9 @@ export type IScenarioFormInput = {
loopbackPeriod: number | undefined
}
export type IScenarioList = {
list: Scenario[]
indicators?: Indicator[]
setScenarios?: React.Dispatch<React.SetStateAction<Scenario[]>>
list: ScenarioViewModel[]
indicators?: IndicatorViewModel[]
setScenarios?: React.Dispatch<React.SetStateAction<ScenarioViewModel[]>>
}
export type IMoneyManagementList = {

View File

@@ -1,11 +1,11 @@
import React, { useState } from 'react'
import React, {useState} from 'react'
import 'react-toastify/dist/ReactToastify.css'
import { BacktestTable } from '../../components/organism'
import BacktestModal from '../../components/organism/Backtest/backtestModal'
import type { Backtest } from '../../generated/ManagingApi'
import UnifiedTradingModal from '../../components/organism/UnifiedTradingModal'
import type {Backtest} from '../../generated/ManagingApi'
const BacktestLoop: React.FC = () => {
const [backtestingResult, setBacktestingResult] = useState<Backtest[]>([])
const [backtestingResult, setBacktest] = useState<Backtest[]>([])
const [showModal, setShowModal] = useState(false)
@@ -20,13 +20,13 @@ const BacktestLoop: React.FC = () => {
return (
<div className="container mx-auto">
<button className="btn" onClick={openModal}>
Run optimized loop
Run Backtest with Loop
</button>
<BacktestTable list={backtestingResult} isFetching={false} />
<BacktestModal
<UnifiedTradingModal
mode="backtest"
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktestingResult}
setBacktests={setBacktest}
showLoopSlider={true}
/>
</div>

View File

@@ -1,8 +1,8 @@
import React, { useState } from 'react'
import React, {useState} from 'react'
import 'react-toastify/dist/ReactToastify.css'
import { BacktestCards, BacktestModal } from '../../components/organism'
import type { Backtest } from '../../generated/ManagingApi'
import {BacktestCards, UnifiedTradingModal} from '../../components/organism'
import type {Backtest} from '../../generated/ManagingApi'
const BacktestPlayground: React.FC = () => {
const [backtestingResult, setBacktest] = useState<Backtest[]>([])
@@ -23,7 +23,8 @@ const BacktestPlayground: React.FC = () => {
Run New Backtest
</button>
<BacktestCards list={backtestingResult} setBacktests={setBacktest} />
<BacktestModal
<UnifiedTradingModal
mode="backtest"
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktest}

View File

@@ -6,7 +6,7 @@ import 'react-toastify/dist/ReactToastify.css'
import useApiUrlStore from '../../app/store/apiStore'
import {Loader} from '../../components/atoms'
import {Modal, Toast} from '../../components/mollecules'
import {BacktestModal, BacktestTable} from '../../components/organism'
import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
import type {Backtest} from '../../generated/ManagingApi'
import {BacktestClient} from '../../generated/ManagingApi'
@@ -101,7 +101,8 @@ const BacktestScanner: React.FC = () => {
<BacktestTable list={backtestingResult} isFetching={isLoading} setBacktests={setBacktest} />
<BacktestModal
<UnifiedTradingModal
mode="backtest"
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktest}

View File

@@ -5,8 +5,7 @@ import useApiUrlStore from '../../app/store/apiStore'
import {CardPosition, CardSignal, CardText, Toast,} from '../../components/mollecules'
import ManualPositionModal from '../../components/mollecules/ManualPositionModal'
import TradesModal from '../../components/mollecules/TradesModal/TradesModal'
import BotConfigModal from '../../components/mollecules/BotConfigModal/BotConfigModal'
import {TradeChart} from '../../components/organism'
import {TradeChart, UnifiedTradingModal} from '../../components/organism'
import type {BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi'
import {BotClient} from '../../generated/ManagingApi'
import type {IBotList} from '../../global/type'
@@ -40,7 +39,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
const [showTradesModal, setShowTradesModal] = useState(false)
const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null)
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [botConfigModalMode, setBotConfigModalMode] = useState<'create' | 'update'>('create')
const [botConfigModalMode, setBotConfigModalMode] = useState<'createBot' | 'updateBot'>('createBot')
const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{
identifier: string
config: any
@@ -219,13 +218,13 @@ const BotList: React.FC<IBotList> = ({ list }) => {
}
function openCreateBotModal() {
setBotConfigModalMode('create')
setBotConfigModalMode('createBot')
setSelectedBotForUpdate(null)
setShowBotConfigModal(true)
}
function openUpdateBotModal(bot: TradingBotResponse) {
setBotConfigModalMode('update')
setBotConfigModalMode('updateBot')
setSelectedBotForUpdate({
identifier: bot.identifier,
config: bot.config
@@ -339,14 +338,17 @@ const BotList: React.FC<IBotList> = ({ list }) => {
setSelectedBotForTrades(null)
}}
/>
<BotConfigModal
showModal={showBotConfigModal}
<UnifiedTradingModal
mode={botConfigModalMode}
existingBot={selectedBotForUpdate || undefined}
onClose={() => {
showModal={showBotConfigModal}
closeModal={() => {
setShowBotConfigModal(false)
setSelectedBotForUpdate(null)
}}
existingBot={selectedBotForUpdate ? {
identifier: selectedBotForUpdate.identifier,
config: selectedBotForUpdate.config
} : undefined}
/>
</div>
)

View File

@@ -5,7 +5,7 @@ import 'react-toastify/dist/ReactToastify.css'
import useApiUrlStore from '../../app/store/apiStore'
import {Toast} from '../../components/mollecules'
import type {Indicator} from '../../generated/ManagingApi'
import type {IndicatorViewModel} from '../../generated/ManagingApi'
import {IndicatorType, ScenarioClient, Timeframe,} from '../../generated/ManagingApi'
import IndicatorTable from './indicatorTable'
@@ -28,7 +28,7 @@ const IndicatorList: React.FC = () => {
const [indicatorType, setIndicatorType] = useState<IndicatorType>(
IndicatorType.RsiDivergence
)
const [indicators, setIndicators] = useState<Indicator[]>([])
const [indicators, setIndicators] = useState<IndicatorViewModel[]>([])
const [showModal, setShowModal] = useState(false)
const { register, handleSubmit } = useForm<IIndicatorFormInput>()
const { apiUrl } = useApiUrlStore()
@@ -49,7 +49,7 @@ const IndicatorList: React.FC = () => {
form.smoothPeriods,
form.cyclePeriods
)
.then((indicator: Indicator) => {
.then((indicator: IndicatorViewModel) => {
t.update('success', 'Indicator created')
setIndicators((arr) => [...arr, indicator])
})
@@ -68,7 +68,7 @@ const IndicatorList: React.FC = () => {
}
useEffect(() => {
scenarioClient.scenario_GetIndicators().then((data: Indicator[]) => {
scenarioClient.scenario_GetIndicators().then((data: IndicatorViewModel[]) => {
setIndicators(data)
})
}, [])

View File

@@ -3,15 +3,15 @@ import React, {useEffect, useState} from 'react'
import useApiUrlStore from '../../app/store/apiStore'
import {SelectColumnFilter, Table, Toast} from '../../components/mollecules'
import type {Indicator} from '../../generated/ManagingApi'
import type {IndicatorViewModel} from '../../generated/ManagingApi'
import {ScenarioClient} from '../../generated/ManagingApi'
interface IIndicatorList {
list: Indicator[]
list: IndicatorViewModel[]
}
const IndicatorTable: React.FC<IIndicatorList> = ({ list }) => {
const [rows, setRows] = useState<Indicator[]>([])
const [rows, setRows] = useState<IndicatorViewModel[]>([])
const { apiUrl } = useApiUrlStore()
async function deleteIndicator(name: string) {
@@ -41,12 +41,6 @@ const IndicatorTable: React.FC<IIndicatorList> = ({ list }) => {
accessor: 'type',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Timeframe',
accessor: 'timeframe',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Signal',

View File

@@ -17,7 +17,7 @@ const tabs: TabsType = [
{
Component: IndicatorList,
index: 2,
label: 'Strategies',
label: 'Indicators',
},
]

View File

@@ -4,15 +4,15 @@ import 'react-toastify/dist/ReactToastify.css'
import useApiUrlStore from '../../app/store/apiStore'
import {Toast} from '../../components/mollecules'
import {ScenarioModal} from '../../components/organism'
import type {Indicator, Scenario} from '../../generated/ManagingApi'
import type {IndicatorViewModel, ScenarioViewModel} from '../../generated/ManagingApi'
import {ScenarioClient} from '../../generated/ManagingApi'
import type {IScenarioFormInput} from '../../global/type'
import ScenarioTable from './scenarioTable'
const ScenarioList: React.FC = () => {
const [indicators, setIndicators] = useState<Indicator[]>([])
const [scenarios, setScenarios] = useState<Scenario[]>([])
const [indicators, setIndicators] = useState<IndicatorViewModel[]>([])
const [scenarios, setScenarios] = useState<ScenarioViewModel[]>([])
const [showModal, setShowModal] = useState(false)
const { apiUrl } = useApiUrlStore()
const client = new ScenarioClient({}, apiUrl)
@@ -21,7 +21,7 @@ const ScenarioList: React.FC = () => {
const t = new Toast('Creating scenario')
await client
.scenario_CreateScenario(form.name, form.loopbackPeriod, form.indicators)
.then((data: Scenario) => {
.then((data: ScenarioViewModel) => {
t.update('success', 'Scenario created')
setScenarios((arr) => [...arr, data])
})

View File

@@ -4,14 +4,14 @@ import React, {useEffect, useState} from 'react'
import useApiUrlStore from '../../app/store/apiStore'
import {Table, Toast} from '../../components/mollecules'
import {ScenarioModal} from '../../components/organism'
import type {Indicator, Scenario} from '../../generated/ManagingApi'
import type {IndicatorViewModel, ScenarioViewModel} from '../../generated/ManagingApi'
import {ScenarioClient} from '../../generated/ManagingApi'
import type {IScenarioFormInput, IScenarioList} from '../../global/type'
const ScenarioTable: React.FC<IScenarioList> = ({ list, indicators = [], setScenarios }) => {
const [rows, setRows] = useState<Scenario[]>([])
const [rows, setRows] = useState<ScenarioViewModel[]>([])
const [showUpdateModal, setShowUpdateModal] = useState(false)
const [selectedScenario, setSelectedScenario] = useState<Scenario | null>(null)
const [selectedScenario, setSelectedScenario] = useState<ScenarioViewModel | null>(null)
const { apiUrl } = useApiUrlStore()
const client = new ScenarioClient({}, apiUrl)
@@ -53,7 +53,7 @@ const ScenarioTable: React.FC<IScenarioList> = ({ list, indicators = [], setScen
})
}
function openUpdateModal(scenario: Scenario) {
function openUpdateModal(scenario: ScenarioViewModel) {
setSelectedScenario(scenario)
setShowUpdateModal(true)
}
@@ -77,7 +77,7 @@ const ScenarioTable: React.FC<IScenarioList> = ({ list, indicators = [], setScen
{
Cell: ({ cell }: any) => (
<>
{cell.row.values.indicators.map((indicator: Indicator) => (
{cell.row.values.indicators.map((indicator: IndicatorViewModel) => (
<div
key={indicator.name}
className="badge badge-primary badge-outline"

View File

@@ -1,7 +1,7 @@
import {ChevronDownIcon, ChevronRightIcon,} from '@heroicons/react/solid'
import React, {useEffect, useMemo, useState} from 'react'
import {useNavigate} from 'react-router-dom'
import {FiKey, FiPlay, FiTrash2, FiTrendingUp} from 'react-icons/fi'
import {FiCopy, FiKey, FiPlay, FiTrash2, FiTrendingUp} from 'react-icons/fi'
import useApiUrlStore from '../../../app/store/apiStore'
import {SelectColumnFilter, Table, Toast,} from '../../../components/mollecules'
@@ -55,6 +55,16 @@ const AccountTable: React.FC<IAccountList> = ({ list, isFetching }) => {
}
}
async function copyToClipboard(text: string) {
const t = new Toast('Copying to clipboard...')
try {
await navigator.clipboard.writeText(text)
t.update('success', 'Address copied to clipboard!')
} catch (err) {
t.update('error', 'Failed to copy to clipboard')
}
}
const columns = useMemo(
() => [
{
@@ -97,9 +107,18 @@ const AccountTable: React.FC<IAccountList> = ({ list, isFetching }) => {
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip={cell.row.values.key}>
{cell.row.values.key.substring(0, 6)}...
{cell.row.values.key.slice(-4)}
<div className="flex items-center space-x-2">
<div className="tooltip" data-tip={cell.row.values.key}>
{cell.row.values.key.substring(0, 6)}...
{cell.row.values.key.slice(-4)}
</div>
<button
className="btn btn-xs btn-ghost"
onClick={() => copyToClipboard(cell.row.values.key)}
title="Copy full address"
>
<FiCopy className="h-3 w-3" />
</button>
</div>
</>
),