feat(backend): add admin evaluation history, engine, simulation, bot API controllers and global exception middleware
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Dtos;
|
||||
using FinlyticCore.Dtos.Simulation;
|
||||
using FinlyticBackend.Util;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticBackend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/simulation")]
|
||||
public class SimulationController : ControllerBase
|
||||
{
|
||||
private readonly BackendMqttBridge _mqttBridge;
|
||||
private readonly ILogger<SimulationController> _logger;
|
||||
|
||||
public SimulationController(BackendMqttBridge mqttBridge, ILogger<SimulationController> logger)
|
||||
{
|
||||
_mqttBridge = mqttBridge;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("run")]
|
||||
public async Task<IActionResult> RunBacktest([FromBody] BacktestRequestDto request)
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.Isin) || string.IsNullOrWhiteSpace(request.StrategyKey))
|
||||
{
|
||||
return BadRequest(new { Message = "ISIN and StrategyKey are required for backtesting." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Starting backtest via REST for {Isin} ({StrategyKey}) on {Timeframe}", request.Isin, request.StrategyKey, request.Timeframe);
|
||||
|
||||
var report = await _mqttBridge.SendRpcRequestAsync<BacktestReportDto, BacktestRequestDto>(
|
||||
"sim_RunBacktest",
|
||||
request,
|
||||
TimeSpan.FromSeconds(25)
|
||||
);
|
||||
|
||||
if (report == null)
|
||||
{
|
||||
return StatusCode(504, new { Message = "Simulation service timed out or did not return a report." });
|
||||
}
|
||||
|
||||
return Ok(report);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error running backtest for {Isin}", request.Isin);
|
||||
return StatusCode(500, new { Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("matrix/{isin}")]
|
||||
public async Task<IActionResult> GetStrategyMatrix(string isin)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(isin))
|
||||
{
|
||||
return BadRequest(new { Message = "ISIN is required." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var matrix = await _mqttBridge.SendRpcRequestAsync<List<StrategyAssetReliabilityDto>, IsinRequest>(
|
||||
"sim_GetMatrixForAsset",
|
||||
new IsinRequest(isin, null, false),
|
||||
TimeSpan.FromSeconds(5)
|
||||
);
|
||||
|
||||
return Ok(matrix ?? new List<StrategyAssetReliabilityDto>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching strategy matrix for {Isin}", isin);
|
||||
return StatusCode(500, new { Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight history of past backtest runs for an ISIN, optionally narrowed to one strategy - every run
|
||||
/// is already persisted server-side (<c>sim_RunBacktest</c>) but was previously only reachable indirectly
|
||||
/// via the reliability matrix, never queryable as a history in its own right.
|
||||
/// </summary>
|
||||
[HttpGet("history/{isin}")]
|
||||
public async Task<IActionResult> GetBacktestHistory(string isin, [FromQuery] string? strategyKey = null, [FromQuery] int limit = 20)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(isin))
|
||||
{
|
||||
return BadRequest(new { Message = "ISIN is required." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var history = await _mqttBridge.SendRpcRequestAsync<List<BacktestHistoryEntryDto>, GetBacktestHistoryRequest>(
|
||||
"sim_GetBacktestHistory",
|
||||
new GetBacktestHistoryRequest(isin, strategyKey, limit),
|
||||
TimeSpan.FromSeconds(5)
|
||||
);
|
||||
|
||||
return Ok(history ?? new List<BacktestHistoryEntryDto>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching backtest history for {Isin}", isin);
|
||||
return StatusCode(500, new { Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Full report (trades + equity curve) for one specific past backtest run, for drilling into a history entry.</summary>
|
||||
[HttpGet("history/run/{runId:guid}")]
|
||||
public async Task<IActionResult> GetBacktestRunDetail(Guid runId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var report = await _mqttBridge.SendRpcRequestAsync<BacktestReportDto, GetBacktestRunDetailRequest>(
|
||||
"sim_GetBacktestRunDetail",
|
||||
new GetBacktestRunDetailRequest(runId),
|
||||
TimeSpan.FromSeconds(5)
|
||||
);
|
||||
|
||||
if (report == null)
|
||||
{
|
||||
return NotFound(new { Message = $"No backtest run found for RunId {runId}." });
|
||||
}
|
||||
|
||||
return Ok(report);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching backtest run detail for {RunId}", runId);
|
||||
return StatusCode(500, new { Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Saved indicator-parameter profile for one (Isin, StrategyKey) pair, or 404 if none was ever saved.</summary>
|
||||
[HttpGet("parameters/{isin}/{strategyKey}")]
|
||||
public async Task<IActionResult> GetStrategyParameters(string isin, string strategyKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(isin) || string.IsNullOrWhiteSpace(strategyKey))
|
||||
{
|
||||
return BadRequest(new { Message = "ISIN and StrategyKey are required." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var profile = await _mqttBridge.SendRpcRequestAsync<StrategyParameterProfileDto, GetStrategyParametersRequest>(
|
||||
"sim_GetStrategyParameters",
|
||||
new GetStrategyParametersRequest(isin, strategyKey),
|
||||
TimeSpan.FromSeconds(5)
|
||||
);
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
return NotFound(new { Message = $"No saved parameter profile for {isin}/{strategyKey}." });
|
||||
}
|
||||
|
||||
return Ok(profile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching strategy parameters for {Isin}/{StrategyKey}", isin, strategyKey);
|
||||
return StatusCode(500, new { Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Saves/updates a per-asset/per-strategy indicator-parameter profile for future backtests to reuse.</summary>
|
||||
[HttpPost("parameters")]
|
||||
public async Task<IActionResult> SaveStrategyParameters([FromBody] SaveStrategyParametersRequest request)
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.Isin) || string.IsNullOrWhiteSpace(request.StrategyKey))
|
||||
{
|
||||
return BadRequest(new { Message = "ISIN and StrategyKey are required." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var profile = await _mqttBridge.SendRpcRequestAsync<StrategyParameterProfileDto, SaveStrategyParametersRequest>(
|
||||
"sim_SaveStrategyParameters",
|
||||
request,
|
||||
TimeSpan.FromSeconds(5)
|
||||
);
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
return StatusCode(504, new { Message = "Simulation service timed out or did not confirm the save." });
|
||||
}
|
||||
|
||||
return Ok(profile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving strategy parameters for {Isin}/{StrategyKey}", request.Isin, request.StrategyKey);
|
||||
return StatusCode(500, new { Message = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user