Add endpoint for indicator refiner

This commit is contained in:
2025-12-28 20:38:38 +07:00
parent d1924d9030
commit 4f3ec31501
5 changed files with 278 additions and 0 deletions

View File

@@ -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
});
}
}
/// <summary>
/// Calculates indicator values and generates signals for a given ticker, timeframe, and date range with selected indicators.
/// </summary>
/// <param name="request">The request containing ticker, timeframe, date range, and indicators configuration.</param>
/// <returns>A response containing calculated indicator values and generated signals.</returns>
[HttpPost("RefineIndicators")]
public async Task<ActionResult<RefineIndicatorsResponse>> 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<IndicatorRequest>
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}");
}
}
}

View File

@@ -0,0 +1,42 @@
using System.ComponentModel.DataAnnotations;
using Managing.Domain.Backtests;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for refining indicators and generating signals.
/// </summary>
public class RefineIndicatorsRequest
{
/// <summary>
/// The ticker symbol (e.g., "BTC", "ETH").
/// </summary>
[Required]
public Ticker Ticker { get; set; }
/// <summary>
/// The timeframe for the candles (e.g., "FifteenMinutes", "OneHour", "OneDay").
/// </summary>
[Required]
public Timeframe Timeframe { get; set; }
/// <summary>
/// The start date for the date range (ISO 8601 format).
/// </summary>
[Required]
public DateTime StartDate { get; set; }
/// <summary>
/// The end date for the date range (ISO 8601 format).
/// </summary>
[Required]
public DateTime EndDate { get; set; }
/// <summary>
/// Array of selected indicators with their parameters.
/// </summary>
[Required]
public List<IndicatorRequest> Indicators { get; set; } = new();
}

View File

@@ -0,0 +1,23 @@
using Managing.Domain.Indicators;
using Managing.Domain.Strategies.Base;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Responses;
/// <summary>
/// Response model for refined indicators and signals.
/// </summary>
public class RefineIndicatorsResponse
{
/// <summary>
/// Dictionary of indicator types to their calculated values over time.
/// </summary>
public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; } =
new Dictionary<IndicatorType, IndicatorsResultBase>();
/// <summary>
/// Array of signals generated for the date range.
/// </summary>
public List<LightSignal> Signals { get; set; } = new List<LightSignal>();
}

View File

@@ -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<IEnumerable<Position>> GetPositionByUserIdAsync(int userId);
Task<SwapInfos> SwapGmxTokensAsync(User user, string accountName, Ticker fromTicker, Ticker toTicker,
double amount, string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5);
/// <summary>
/// Calculates indicator values and generates signals for a given ticker, timeframe, and date range with selected indicators.
/// </summary>
Task<RefineIndicatorsResult> RefineIndicatorsAsync(
Ticker ticker,
Timeframe timeframe,
DateTime startDate,
DateTime endDate,
List<IndicatorRequest> indicators);
}
/// <summary>
/// Result of refining indicators operation.
/// </summary>
public class RefineIndicatorsResult
{
public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; } =
new Dictionary<IndicatorType, IndicatorsResultBase>();
public List<LightSignal> Signals { get; set; } = new List<LightSignal>();
}

View File

@@ -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);
}
}
/// <summary>
/// Calculates indicator values and generates signals for a given ticker, timeframe, and date range with selected indicators.
/// </summary>
public async Task<RefineIndicatorsResult> RefineIndicatorsAsync(
Ticker ticker,
Timeframe timeframe,
DateTime startDate,
DateTime endDate,
List<IndicatorRequest> 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<IndicatorType, IndicatorsResultBase>(),
Signals = new List<LightSignal>()
};
}
// 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
};
}
/// <summary>
/// Maps IndicatorRequest list to a domain Scenario object.
/// </summary>
private Scenario MapRefineIndicatorsToScenario(List<IndicatorRequest> 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;
}
/// <summary>
/// Generates signals for a date range using a rolling window approach.
/// </summary>
private List<LightSignal> GenerateSignalsForDateRange(
List<Candle> candles,
Scenario scenario,
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
{
var allSignals = new List<LightSignal>();
var previousSignals = new Dictionary<string, LightSignal>();
var lightScenario = LightScenario.FromScenario(scenario);
// Use rolling window approach similar to backtests
const int RollingWindowSize = 600;
var rollingWindowCandles = new List<Candle>(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();
}
}