diff --git a/src/Managing.Api/Controllers/TradingController.cs b/src/Managing.Api/Controllers/TradingController.cs index fc215a96..bd053d18 100644 --- a/src/Managing.Api/Controllers/TradingController.cs +++ b/src/Managing.Api/Controllers/TradingController.cs @@ -1,6 +1,7 @@ using System.Net.Http.Headers; using System.Text; using Managing.Api.Models.Requests; +using Managing.Api.Models.Responses; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Shared; @@ -340,4 +341,54 @@ public class TradingController : BaseController }); } } + + /// + /// Calculates indicator values and generates signals for a given ticker, timeframe, and date range with selected indicators. + /// + /// The request containing ticker, timeframe, date range, and indicators configuration. + /// A response containing calculated indicator values and generated signals. + [HttpPost("RefineIndicators")] + public async Task> RefineIndicators( + [FromBody] RefineIndicatorsRequest request) + { + try + { + // Validate request + if (request == null) + { + return BadRequest("Request cannot be null."); + } + + if (request.Indicators == null || request.Indicators.Count == 0) + { + return BadRequest("At least one indicator must be provided."); + } + + if (request.StartDate >= request.EndDate) + { + return BadRequest("Start date must be before end date."); + } + + // Call service - request.Indicators is already List + var result = await _tradingService.RefineIndicatorsAsync( + request.Ticker, + request.Timeframe, + request.StartDate, + request.EndDate, + request.Indicators); + + // Map service result to API response + return Ok(new RefineIndicatorsResponse + { + IndicatorsValues = result.IndicatorsValues, + Signals = result.Signals + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refining indicators for ticker {Ticker}, timeframe {Timeframe}", + request?.Ticker, request?.Timeframe); + return StatusCode(500, $"Error refining indicators: {ex.Message}"); + } + } } \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/RefineIndicatorsRequest.cs b/src/Managing.Api/Models/Requests/RefineIndicatorsRequest.cs new file mode 100644 index 00000000..afa62561 --- /dev/null +++ b/src/Managing.Api/Models/Requests/RefineIndicatorsRequest.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; +using Managing.Domain.Backtests; +using static Managing.Common.Enums; + +namespace Managing.Api.Models.Requests; + +/// +/// Request model for refining indicators and generating signals. +/// +public class RefineIndicatorsRequest +{ + /// + /// The ticker symbol (e.g., "BTC", "ETH"). + /// + [Required] + public Ticker Ticker { get; set; } + + /// + /// The timeframe for the candles (e.g., "FifteenMinutes", "OneHour", "OneDay"). + /// + [Required] + public Timeframe Timeframe { get; set; } + + /// + /// The start date for the date range (ISO 8601 format). + /// + [Required] + public DateTime StartDate { get; set; } + + /// + /// The end date for the date range (ISO 8601 format). + /// + [Required] + public DateTime EndDate { get; set; } + + /// + /// Array of selected indicators with their parameters. + /// + [Required] + public List Indicators { get; set; } = new(); +} + diff --git a/src/Managing.Api/Models/Responses/RefineIndicatorsResponse.cs b/src/Managing.Api/Models/Responses/RefineIndicatorsResponse.cs new file mode 100644 index 00000000..d20b568a --- /dev/null +++ b/src/Managing.Api/Models/Responses/RefineIndicatorsResponse.cs @@ -0,0 +1,23 @@ +using Managing.Domain.Indicators; +using Managing.Domain.Strategies.Base; +using static Managing.Common.Enums; + +namespace Managing.Api.Models.Responses; + +/// +/// Response model for refined indicators and signals. +/// +public class RefineIndicatorsResponse +{ + /// + /// Dictionary of indicator types to their calculated values over time. + /// + public Dictionary IndicatorsValues { get; set; } = + new Dictionary(); + + /// + /// Array of signals generated for the date range. + /// + public List Signals { get; set; } = new List(); +} + diff --git a/src/Managing.Application.Abstractions/Services/ITradingService.cs b/src/Managing.Application.Abstractions/Services/ITradingService.cs index 17fd79e9..2cc3ccd1 100644 --- a/src/Managing.Application.Abstractions/Services/ITradingService.cs +++ b/src/Managing.Application.Abstractions/Services/ITradingService.cs @@ -1,9 +1,11 @@ using Managing.Domain.Accounts; +using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Indicators; using Managing.Domain.Scenarios; using Managing.Domain.Statistics; using Managing.Domain.Strategies; +using Managing.Domain.Strategies.Base; using Managing.Domain.Synth.Models; using Managing.Domain.Trades; using Managing.Domain.Users; @@ -57,4 +59,24 @@ public interface ITradingService Task> GetPositionByUserIdAsync(int userId); Task SwapGmxTokensAsync(User user, string accountName, Ticker fromTicker, Ticker toTicker, double amount, string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5); + + /// + /// Calculates indicator values and generates signals for a given ticker, timeframe, and date range with selected indicators. + /// + Task RefineIndicatorsAsync( + Ticker ticker, + Timeframe timeframe, + DateTime startDate, + DateTime endDate, + List indicators); +} + +/// +/// Result of refining indicators operation. +/// +public class RefineIndicatorsResult +{ + public Dictionary IndicatorsValues { get; set; } = + new Dictionary(); + public List Signals { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Managing.Application/Trading/TradingService.cs b/src/Managing.Application/Trading/TradingService.cs index ef529744..fdf1d49e 100644 --- a/src/Managing.Application/Trading/TradingService.cs +++ b/src/Managing.Application/Trading/TradingService.cs @@ -1,12 +1,15 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Domain.Accounts; +using Managing.Domain.Backtests; using Managing.Domain.Bots; +using Managing.Domain.Candles; using Managing.Domain.Indicators; using Managing.Domain.Scenarios; using Managing.Domain.Shared.Helpers; using Managing.Domain.Statistics; using Managing.Domain.Strategies; +using Managing.Domain.Strategies.Base; using Managing.Domain.Synth.Models; using Managing.Domain.Trades; using Managing.Domain.Users; @@ -491,4 +494,141 @@ public class TradingService : ITradingService throw new InvalidOperationException($"Failed to swap GMX tokens: {ex.Message}", ex); } } + + /// + /// Calculates indicator values and generates signals for a given ticker, timeframe, and date range with selected indicators. + /// + public async Task RefineIndicatorsAsync( + Ticker ticker, + Timeframe timeframe, + DateTime startDate, + DateTime endDate, + List indicators) + { + // Get candles for the specified period + var candles = await _exchangeService.GetCandlesInflux( + TradingExchanges.Evm, + ticker, + startDate, + timeframe, + endDate); + + if (candles == null || candles.Count == 0) + { + return new RefineIndicatorsResult + { + IndicatorsValues = new Dictionary(), + Signals = new List() + }; + } + + // Convert to ordered List to preserve chronological order for indicators + var candlesList = candles.OrderBy(c => c.Date).ToList(); + + // Map request indicators to domain Scenario + var scenario = MapRefineIndicatorsToScenario(indicators); + + // Calculate indicators values + var indicatorsValues = TradingBox.CalculateIndicatorsValues(scenario, candlesList); + + // Generate signals for the date range using rolling window approach + var signals = GenerateSignalsForDateRange(candlesList, scenario, indicatorsValues); + + return new RefineIndicatorsResult + { + IndicatorsValues = indicatorsValues, + Signals = signals + }; + } + + /// + /// Maps IndicatorRequest list to a domain Scenario object. + /// + private Scenario MapRefineIndicatorsToScenario(List indicators) + { + var scenario = new Scenario("RefineIndicators", 1); + + foreach (var indicatorRequest in indicators) + { + var indicator = new IndicatorBase(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, + StDev = indicatorRequest.StDev, + SmoothPeriods = indicatorRequest.SmoothPeriods, + StochPeriods = indicatorRequest.StochPeriods, + CyclePeriods = indicatorRequest.CyclePeriods, + KFactor = indicatorRequest.KFactor, + DFactor = indicatorRequest.DFactor, + TenkanPeriods = indicatorRequest.TenkanPeriods, + KijunPeriods = indicatorRequest.KijunPeriods, + SenkouBPeriods = indicatorRequest.SenkouBPeriods, + OffsetPeriods = indicatorRequest.OffsetPeriods, + SenkouOffset = indicatorRequest.SenkouOffset, + ChikouOffset = indicatorRequest.ChikouOffset + }; + + scenario.AddIndicator(indicator); + } + + return scenario; + } + + /// + /// Generates signals for a date range using a rolling window approach. + /// + private List GenerateSignalsForDateRange( + List candles, + Scenario scenario, + Dictionary preCalculatedIndicatorValues) + { + var allSignals = new List(); + var previousSignals = new Dictionary(); + var lightScenario = LightScenario.FromScenario(scenario); + + // Use rolling window approach similar to backtests + const int RollingWindowSize = 600; + var rollingWindowCandles = new List(RollingWindowSize); + + foreach (var candle in candles) + { + // Maintain rolling window + if (rollingWindowCandles.Count >= RollingWindowSize) + { + rollingWindowCandles.RemoveAt(0); + } + rollingWindowCandles.Add(candle); + + // Only process if we have enough candles for indicators + if (rollingWindowCandles.Count < 2) + { + continue; + } + + // Generate signal for current position + var signal = TradingBox.GetSignal( + rollingWindowCandles, + lightScenario, + previousSignals, + scenario.LookbackPeriod, + preCalculatedIndicatorValues); + + if (signal != null) + { + // Check if this is a new signal (not already in our collection) + if (!previousSignals.ContainsKey(signal.Identifier)) + { + allSignals.Add(signal); + previousSignals[signal.Identifier] = signal; + } + } + } + + return allSignals.OrderBy(s => s.Date).ToList(); + } } \ No newline at end of file