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 _logger; public SimulationController(BackendMqttBridge mqttBridge, ILogger logger) { _mqttBridge = mqttBridge; _logger = logger; } [HttpPost("run")] public async Task 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( "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 GetStrategyMatrix(string isin) { if (string.IsNullOrWhiteSpace(isin)) { return BadRequest(new { Message = "ISIN is required." }); } try { var matrix = await _mqttBridge.SendRpcRequestAsync, IsinRequest>( "sim_GetMatrixForAsset", new IsinRequest(isin, null, false), TimeSpan.FromSeconds(5) ); return Ok(matrix ?? new List()); } catch (Exception ex) { _logger.LogError(ex, "Error fetching strategy matrix for {Isin}", isin); return StatusCode(500, new { Message = ex.Message }); } } /// /// Lightweight history of past backtest runs for an ISIN, optionally narrowed to one strategy - every run /// is already persisted server-side (sim_RunBacktest) but was previously only reachable indirectly /// via the reliability matrix, never queryable as a history in its own right. /// [HttpGet("history/{isin}")] public async Task 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, GetBacktestHistoryRequest>( "sim_GetBacktestHistory", new GetBacktestHistoryRequest(isin, strategyKey, limit), TimeSpan.FromSeconds(5) ); return Ok(history ?? new List()); } catch (Exception ex) { _logger.LogError(ex, "Error fetching backtest history for {Isin}", isin); return StatusCode(500, new { Message = ex.Message }); } } /// Full report (trades + equity curve) for one specific past backtest run, for drilling into a history entry. [HttpGet("history/run/{runId:guid}")] public async Task GetBacktestRunDetail(Guid runId) { try { var report = await _mqttBridge.SendRpcRequestAsync( "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 }); } } /// Saved indicator-parameter profile for one (Isin, StrategyKey) pair, or 404 if none was ever saved. [HttpGet("parameters/{isin}/{strategyKey}")] public async Task 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( "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 }); } } /// Saves/updates a per-asset/per-strategy indicator-parameter profile for future backtests to reuse. [HttpPost("parameters")] public async Task 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( "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 }); } } }