Add synthApi (#27)

* Add synthApi

* Put confidence for Synth proba

* Update the code

* Update readme

* Fix bootstraping

* fix github build

* Update the endpoints for scenario

* Add scenario and update backtest modal

* Update bot modal

* Update interfaces for synth

* add synth to backtest

* Add Kelly criterion and better signal

* Update signal confidence

* update doc

* save leaderboard and prediction

* Update nswag to generate ApiClient in the correct path

* Unify the trading modal

* Save miner and prediction

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

* Rename strategies to indicators

* Update doc

* Update chart + add signal name

* Fix signal direction

* Update docker webui

* remove crypto npm

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

View File

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

View File

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

View File

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

View File

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