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:
@@ -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; }
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
73
src/Managing.Api/Models/Requests/IndicatorRequest.cs
Normal file
73
src/Managing.Api/Models/Requests/IndicatorRequest.cs
Normal 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; }
|
||||
}
|
||||
13
src/Managing.Api/Models/Requests/MoneyManagementRequest.cs
Normal file
13
src/Managing.Api/Models/Requests/MoneyManagementRequest.cs
Normal 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
26
src/Managing.Api/Models/Requests/ScenarioRequest.cs
Normal file
26
src/Managing.Api/Models/Requests/ScenarioRequest.cs
Normal 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
119
src/Managing.Api/Models/Requests/TradingBotConfigRequest.cs
Normal file
119
src/Managing.Api/Models/Requests/TradingBotConfigRequest.cs
Normal 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;
|
||||
}
|
||||
32
src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs
Normal file
32
src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs
Normal 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; }
|
||||
}
|
||||
31
src/Managing.Api/Models/Responses/IndicatorViewModel.cs
Normal file
31
src/Managing.Api/Models/Responses/IndicatorViewModel.cs
Normal 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;
|
||||
}
|
||||
17
src/Managing.Api/Models/Responses/ScenarioViewModel.cs
Normal file
17
src/Managing.Api/Models/Responses/ScenarioViewModel.cs
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Managing.Application.Scenarios
|
||||
return _tradingService.GetScenarioByName(name);
|
||||
}
|
||||
|
||||
public IEnumerable<Indicator> GetStrategies()
|
||||
public IEnumerable<Indicator> GetIndicators()
|
||||
{
|
||||
return _tradingService.GetStrategies();
|
||||
}
|
||||
|
||||
324
src/Managing.Application/Synth/SynthApiClient.cs
Normal file
324
src/Managing.Application/Synth/SynthApiClient.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
169
src/Managing.Application/Synth/SynthConfigurationHelper.cs
Normal file
169
src/Managing.Application/Synth/SynthConfigurationHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
1194
src/Managing.Application/Synth/SynthPredictionService.cs
Normal file
1194
src/Managing.Application/Synth/SynthPredictionService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
192
src/Managing.Domain/Risk/RiskManagement.cs
Normal file
192
src/Managing.Domain/Risk/RiskManagement.cs
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Managing.Domain.Strategies.Signals
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
36
src/Managing.Domain/Synth/Models/MinerInfo.cs
Normal file
36
src/Managing.Domain/Synth/Models/MinerInfo.cs
Normal 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; }
|
||||
}
|
||||
32
src/Managing.Domain/Synth/Models/MinerPrediction.cs
Normal file
32
src/Managing.Domain/Synth/Models/MinerPrediction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
18
src/Managing.Domain/Synth/Models/PricePoint.cs
Normal file
18
src/Managing.Domain/Synth/Models/PricePoint.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
403
src/Managing.Domain/Synth/Models/SignalValidationResult.cs
Normal file
403
src/Managing.Domain/Synth/Models/SignalValidationResult.cs
Normal 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}"
|
||||
};
|
||||
}
|
||||
}
|
||||
64
src/Managing.Domain/Synth/Models/SynthConfiguration.cs
Normal file
64
src/Managing.Domain/Synth/Models/SynthConfiguration.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
57
src/Managing.Domain/Synth/Models/SynthMinersLeaderboard.cs
Normal file
57
src/Managing.Domain/Synth/Models/SynthMinersLeaderboard.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
72
src/Managing.Domain/Synth/Models/SynthMinersPredictions.cs
Normal file
72
src/Managing.Domain/Synth/Models/SynthMinersPredictions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
67
src/Managing.Domain/Synth/Models/SynthPrediction.cs
Normal file
67
src/Managing.Domain/Synth/Models/SynthPrediction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
13
src/Managing.Domain/Synth/Models/SynthRiskResult.cs
Normal file
13
src/Managing.Domain/Synth/Models/SynthRiskResult.cs
Normal 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
223
src/Managing.Infrastructure.Database/SynthRepository.cs
Normal file
223
src/Managing.Infrastructure.Database/SynthRepository.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
540
src/Managing.Infrastructure.Tests/SynthPredictionTests.cs
Normal file
540
src/Managing.Infrastructure.Tests/SynthPredictionTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;"]
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
export { default } from './UnifiedTradingModal'
|
||||
@@ -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'
|
||||
|
||||
@@ -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",
|
||||
|
||||
887
src/Managing.WebApp/src/generated/ManagingApiTypes.ts
Normal file
887
src/Managing.WebApp/src/generated/ManagingApiTypes.ts
Normal 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 };
|
||||
}
|
||||
50
src/Managing.WebApp/src/global/type.ts
Normal file
50
src/Managing.WebApp/src/global/type.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -17,7 +17,7 @@ const tabs: TabsType = [
|
||||
{
|
||||
Component: IndicatorList,
|
||||
index: 2,
|
||||
label: 'Strategies',
|
||||
label: 'Indicators',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user