Compare commits
2 Commits
8878ec632e
...
4c4839e36a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c4839e36a | |||
| 88f245a80d |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -178,6 +178,10 @@ DocProject/Help/*.hhk
|
|||||||
DocProject/Help/*.hhp
|
DocProject/Help/*.hhp
|
||||||
DocProject/Help/Html2
|
DocProject/Help/Html2
|
||||||
DocProject/Help/html
|
DocProject/Help/html
|
||||||
|
# DocFx
|
||||||
|
[Dd]ocs/
|
||||||
|
docfx.build.json
|
||||||
|
docfx.metadata.json
|
||||||
|
|
||||||
# Click-Once directory
|
# Click-Once directory
|
||||||
publish/
|
publish/
|
||||||
@@ -412,5 +416,4 @@ FodyWeavers.xsd
|
|||||||
.history/
|
.history/
|
||||||
|
|
||||||
# Built Visual Studio Code Extensions
|
# Built Visual Studio Code Extensions
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
@@ -5,6 +5,9 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace Mercados.Api.Controllers
|
namespace Mercados.Api.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Controlador principal para exponer los datos de los mercados financieros.
|
||||||
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class MercadosController : ControllerBase
|
public class MercadosController : ControllerBase
|
||||||
@@ -15,7 +18,14 @@ namespace Mercados.Api.Controllers
|
|||||||
private readonly IHolidayService _holidayService;
|
private readonly IHolidayService _holidayService;
|
||||||
private readonly ILogger<MercadosController> _logger;
|
private readonly ILogger<MercadosController> _logger;
|
||||||
|
|
||||||
// Inyectamos TODOS los repositorios que necesita el controlador.
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia del controlador MercadosController.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bolsaRepo">Repositorio para datos de la bolsa.</param>
|
||||||
|
/// <param name="granoRepo">Repositorio para datos de granos.</param>
|
||||||
|
/// <param name="ganadoRepo">Repositorio para datos de ganado.</param>
|
||||||
|
/// <param name="holidayService">Servicio para consultar feriados.</param>
|
||||||
|
/// <param name="logger">Servicio de logging.</param>
|
||||||
public MercadosController(
|
public MercadosController(
|
||||||
ICotizacionBolsaRepository bolsaRepo,
|
ICotizacionBolsaRepository bolsaRepo,
|
||||||
ICotizacionGranoRepository granoRepo,
|
ICotizacionGranoRepository granoRepo,
|
||||||
@@ -30,7 +40,10 @@ namespace Mercados.Api.Controllers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Endpoint para Agroganadero ---
|
/// <summary>
|
||||||
|
/// Obtiene el último parte completo del mercado agroganadero.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una colección de objetos CotizacionGanado.</returns>
|
||||||
[HttpGet("agroganadero")]
|
[HttpGet("agroganadero")]
|
||||||
[ProducesResponseType(typeof(IEnumerable<CotizacionGanado>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(IEnumerable<CotizacionGanado>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -48,7 +61,10 @@ namespace Mercados.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Endpoint para Granos ---
|
/// <summary>
|
||||||
|
/// Obtiene las últimas cotizaciones para los principales granos.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una colección de objetos CotizacionGrano.</returns>
|
||||||
[HttpGet("granos")]
|
[HttpGet("granos")]
|
||||||
[ProducesResponseType(typeof(IEnumerable<CotizacionGrano>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(IEnumerable<CotizacionGrano>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -67,6 +83,10 @@ namespace Mercados.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Endpoints de Bolsa ---
|
// --- Endpoints de Bolsa ---
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene las últimas cotizaciones para el mercado de bolsa de EEUU.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una colección de objetos CotizacionBolsa.</returns>
|
||||||
[HttpGet("bolsa/eeuu")]
|
[HttpGet("bolsa/eeuu")]
|
||||||
[ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -84,6 +104,10 @@ namespace Mercados.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene las últimas cotizaciones para el mercado de bolsa local.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una colección de objetos CotizacionBolsa.</returns>
|
||||||
[HttpGet("bolsa/local")]
|
[HttpGet("bolsa/local")]
|
||||||
[ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -101,23 +125,37 @@ namespace Mercados.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("bolsa/history/{ticker}")]
|
/// <summary>
|
||||||
[ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)]
|
/// Obtiene el historial de cotizaciones para un ticker específico en un mercado determinado.
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
/// </summary>
|
||||||
public async Task<IActionResult> GetBolsaHistory(string ticker, [FromQuery] string mercado = "Local", [FromQuery] int dias = 30)
|
/// <param name="ticker">El identificador del ticker.</param>
|
||||||
{
|
/// <param name="mercado">El nombre del mercado (por defecto "Local").</param>
|
||||||
try
|
/// <param name="dias">Cantidad de días de historial a recuperar (por defecto 30).</param>
|
||||||
{
|
/// <returns>Una colección de objetos CotizacionBolsa.</returns>
|
||||||
var data = await _bolsaRepo.ObtenerHistorialPorTickerAsync(ticker, mercado, dias);
|
[HttpGet("bolsa/history/{ticker}")]
|
||||||
return Ok(data);
|
[ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)]
|
||||||
}
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
catch (Exception ex)
|
public async Task<IActionResult> GetBolsaHistory(string ticker, [FromQuery] string mercado = "Local", [FromQuery] int dias = 30)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error al obtener historial para el ticker {Ticker}.", ticker);
|
try
|
||||||
return StatusCode(500, "Ocurrió un error interno en el servidor.");
|
{
|
||||||
}
|
var data = await _bolsaRepo.ObtenerHistorialPorTickerAsync(ticker, mercado, dias);
|
||||||
}
|
return Ok(data);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al obtener historial para el ticker {Ticker}.", ticker);
|
||||||
|
return StatusCode(500, "Ocurrió un error interno en el servidor.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene el historial de cotizaciones para una categoría y especificaciones de ganado en un rango de días.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="categoria">La categoría de ganado.</param>
|
||||||
|
/// <param name="especificaciones">Las especificaciones del ganado.</param>
|
||||||
|
/// <param name="dias">Cantidad de días de historial a recuperar (por defecto 30).</param>
|
||||||
|
/// <returns>Una colección de objetos CotizacionGanado.</returns>
|
||||||
[HttpGet("agroganadero/history")]
|
[HttpGet("agroganadero/history")]
|
||||||
[ProducesResponseType(typeof(IEnumerable<CotizacionGanado>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(IEnumerable<CotizacionGanado>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -135,6 +173,12 @@ namespace Mercados.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene el historial de cotizaciones para un grano específico en un rango de días.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nombre">El nombre del grano.</param>
|
||||||
|
/// <param name="dias">Cantidad de días de historial a recuperar (por defecto 30).</param>
|
||||||
|
/// <returns>Una colección de objetos CotizacionGrano.</returns>
|
||||||
[HttpGet("granos/history/{nombre}")]
|
[HttpGet("granos/history/{nombre}")]
|
||||||
[ProducesResponseType(typeof(IEnumerable<CotizacionGrano>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(IEnumerable<CotizacionGrano>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -152,6 +196,11 @@ namespace Mercados.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica si la fecha actual es feriado para el mercado especificado.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mercado">El nombre del mercado a consultar.</param>
|
||||||
|
/// <returns>True si es feriado, false en caso contrario.</returns>
|
||||||
[HttpGet("es-feriado/{mercado}")]
|
[HttpGet("es-feriado/{mercado}")]
|
||||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>28c6a673-1f1e-4140-aa75-a0d894d1fbc4</UserSecretsId>
|
<UserSecretsId>28c6a673-1f1e-4140-aa75-a0d894d1fbc4</UserSecretsId>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -9,19 +9,32 @@ namespace Mercados.Api.Utils
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class UtcDateTimeConverter : JsonConverter<DateTime>
|
public class UtcDateTimeConverter : JsonConverter<DateTime>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lee un valor DateTime desde el lector JSON y lo convierte a UTC.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">El lector JSON.</param>
|
||||||
|
/// <param name="typeToConvert">El tipo a convertir.</param>
|
||||||
|
/// <param name="options">Las opciones de serialización JSON.</param>
|
||||||
|
/// <returns>El valor DateTime en UTC.</returns>
|
||||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
// Al leer un string de fecha, nos aseguramos de que se interprete como UTC
|
// Al leer un string de fecha, nos aseguramos de que se interprete como UTC
|
||||||
return reader.GetDateTime().ToUniversalTime();
|
return reader.GetDateTime().ToUniversalTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
/// <summary>
|
||||||
{
|
/// Escribe un valor DateTime en formato UTC como una cadena en el escritor JSON.
|
||||||
// Antes de escribir el string, especificamos que el 'Kind' es Utc.
|
/// </summary>
|
||||||
// Si ya es Utc, no hace nada. Si es Local o Unspecified, lo trata como si fuera Utc.
|
/// <param name="writer">El escritor JSON.</param>
|
||||||
// Esto es seguro porque sabemos que todas nuestras fechas en la BD son UTC.
|
/// <param name="value">El valor DateTime a escribir.</param>
|
||||||
var utcValue = DateTime.SpecifyKind(value, DateTimeKind.Utc);
|
/// <param name="options">Las opciones de serialización JSON.</param>
|
||||||
writer.WriteStringValue(utcValue);
|
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||||
}
|
{
|
||||||
|
// Antes de escribir el string, especificamos que el 'Kind' es Utc.
|
||||||
|
// Si ya es Utc, no hace nada. Si es Local o Unspecified, lo trata como si fuera Utc.
|
||||||
|
// Esto es seguro porque sabemos que todas nuestras fechas en la BD son UTC.
|
||||||
|
var utcValue = DateTime.SpecifyKind(value, DateTimeKind.Utc);
|
||||||
|
writer.WriteStringValue(utcValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,53 @@
|
|||||||
namespace Mercados.Core.Entities
|
namespace Mercados.Core.Entities
|
||||||
{
|
{
|
||||||
public class CotizacionBolsa
|
/// <summary>
|
||||||
{
|
/// Representa una única captura de cotización para un activo de la bolsa de valores.
|
||||||
public long Id { get; set; }
|
/// </summary>
|
||||||
public string Ticker { get; set; } = string.Empty; // "AAPL", "GGAL.BA", etc.
|
public class CotizacionBolsa
|
||||||
public string? NombreEmpresa { get; set; }
|
{
|
||||||
public string Mercado { get; set; } = string.Empty; // "EEUU" o "Local"
|
/// <summary>
|
||||||
public decimal PrecioActual { get; set; }
|
/// Identificador único del registro en la base de datos.
|
||||||
public decimal Apertura { get; set; }
|
/// </summary>
|
||||||
public decimal CierreAnterior { get; set; }
|
public long Id { get; set; }
|
||||||
public decimal PorcentajeCambio { get; set; }
|
|
||||||
public DateTime FechaRegistro { get; set; }
|
/// <summary>
|
||||||
}
|
/// El símbolo o identificador del activo en el mercado (ej. "AAPL", "GGAL.BA").
|
||||||
|
/// </summary>
|
||||||
|
public string Ticker { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El nombre completo de la empresa o del activo.
|
||||||
|
/// </summary>
|
||||||
|
public string? NombreEmpresa { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El mercado al que pertenece el activo (ej. "EEUU", "Local").
|
||||||
|
/// </summary>
|
||||||
|
public string Mercado { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El último precio registrado para el activo.
|
||||||
|
/// </summary>
|
||||||
|
public decimal PrecioActual { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El precio del activo al inicio de la jornada de mercado.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Apertura { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El precio de cierre del activo en la jornada anterior.
|
||||||
|
/// </summary>
|
||||||
|
public decimal CierreAnterior { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El cambio porcentual del precio actual con respecto al cierre anterior.
|
||||||
|
/// </summary>
|
||||||
|
public decimal PorcentajeCambio { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La fecha y hora (en UTC) en que se registró esta cotización en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime FechaRegistro { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,68 @@
|
|||||||
namespace Mercados.Core.Entities
|
namespace Mercados.Core.Entities
|
||||||
{
|
{
|
||||||
public class CotizacionGanado
|
/// <summary>
|
||||||
{
|
/// Representa una cotización para una categoría de ganado en el Mercado Agroganadero.
|
||||||
public long Id { get; set; }
|
/// </summary>
|
||||||
public string Categoria { get; set; } = string.Empty;
|
public class CotizacionGanado
|
||||||
public string Especificaciones { get; set; } = string.Empty;
|
{
|
||||||
public decimal Maximo { get; set; }
|
/// <summary>
|
||||||
public decimal Minimo { get; set; }
|
/// Identificador único del registro en la base de datos.
|
||||||
public decimal Promedio { get; set; }
|
/// </summary>
|
||||||
public decimal Mediano { get; set; }
|
public long Id { get; set; }
|
||||||
public int Cabezas { get; set; }
|
|
||||||
public int KilosTotales { get; set; }
|
/// <summary>
|
||||||
public int KilosPorCabeza { get; set; }
|
/// La categoría principal del ganado (ej. "NOVILLOS", "VACAS").
|
||||||
public decimal ImporteTotal { get; set; }
|
/// </summary>
|
||||||
public DateTime FechaRegistro { get; set; }
|
public string Categoria { get; set; } = string.Empty;
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detalles adicionales sobre la categoría, como raza o peso.
|
||||||
|
/// </summary>
|
||||||
|
public string Especificaciones { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El precio máximo alcanzado para esta categoría en la jornada.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Maximo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El precio mínimo alcanzado para esta categoría en la jornada.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Minimo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El precio promedio ponderado para la categoría.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Promedio { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El precio mediano (valor central) registrado para la categoría.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Mediano { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El número total de cabezas de ganado comercializadas en esta categoría.
|
||||||
|
/// </summary>
|
||||||
|
public int Cabezas { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El peso total en kilogramos de todo el ganado comercializado.
|
||||||
|
/// </summary>
|
||||||
|
public int KilosTotales { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El peso promedio por cabeza de ganado.
|
||||||
|
/// </summary>
|
||||||
|
public int KilosPorCabeza { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El importe total monetario de las transacciones para esta categoría.
|
||||||
|
/// </summary>
|
||||||
|
public decimal ImporteTotal { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La fecha y hora (en UTC) en que se registró esta cotización en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime FechaRegistro { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,38 @@
|
|||||||
namespace Mercados.Core.Entities
|
namespace Mercados.Core.Entities
|
||||||
{
|
{
|
||||||
public class CotizacionGrano
|
/// <summary>
|
||||||
{
|
/// Representa una cotización para un tipo de grano específico.
|
||||||
public long Id { get; set; }
|
/// </summary>
|
||||||
public string Nombre { get; set; } = string.Empty; // "Soja", "Trigo", etc.
|
public class CotizacionGrano
|
||||||
public decimal Precio { get; set; }
|
{
|
||||||
public decimal VariacionPrecio { get; set; }
|
/// <summary>
|
||||||
public DateTime FechaOperacion { get; set; }
|
/// Identificador único del registro en la base de datos.
|
||||||
public DateTime FechaRegistro { get; set; }
|
/// </summary>
|
||||||
}
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El nombre del grano (ej. "Soja", "Trigo", "Maíz").
|
||||||
|
/// </summary>
|
||||||
|
public string Nombre { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El precio de cotización, generalmente por tonelada.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Precio { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La variación del precio con respecto a la cotización anterior.
|
||||||
|
/// </summary>
|
||||||
|
public decimal VariacionPrecio { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La fecha en que se concertó la operación de la cotización.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime FechaOperacion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La fecha y hora (en UTC) en que se registró esta cotización en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime FechaRegistro { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,31 @@
|
|||||||
namespace Mercados.Core.Entities
|
namespace Mercados.Core.Entities
|
||||||
{
|
{
|
||||||
public class FuenteDato
|
/// <summary>
|
||||||
{
|
/// Representa una fuente de datos externa desde la cual se obtiene información.
|
||||||
public long Id { get; set; }
|
/// Esta entidad se utiliza para auditar y monitorear la salud de los Data Fetchers.
|
||||||
public string Nombre { get; set; } = string.Empty; // "BCR", "Finnhub", "YahooFinance", "MercadoAgroganadero"
|
/// </summary>
|
||||||
public DateTime UltimaEjecucionExitosa { get; set; }
|
public class FuenteDato
|
||||||
public string? Url { get; set; }
|
{
|
||||||
}
|
/// <summary>
|
||||||
|
/// Identificador único del registro en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// El nombre único que identifica a la fuente de datos (ej. "BCR", "Finnhub", "YahooFinance", "MercadoAgroganadero").
|
||||||
|
/// Este nombre coincide con la propiedad SourceName de la interfaz IDataFetcher.
|
||||||
|
/// </summary>
|
||||||
|
public string Nombre { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La fecha y hora (en UTC) de la última vez que el Data Fetcher correspondiente
|
||||||
|
/// se ejecutó y completó su tarea exitosamente.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime UltimaEjecucionExitosa { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La URL base o principal de la fuente de datos, para referencia.
|
||||||
|
/// </summary>
|
||||||
|
public string? Url { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,28 @@
|
|||||||
namespace Mercados.Core.Entities
|
namespace Mercados.Core.Entities
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Representa un único día feriado para un mercado bursátil específico.
|
||||||
|
/// </summary>
|
||||||
public class MercadoFeriado
|
public class MercadoFeriado
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identificador único del registro en la base de datos.
|
||||||
|
/// </summary>
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
public string CodigoMercado { get; set; } = string.Empty; // "US" o "BA"
|
|
||||||
|
/// <summary>
|
||||||
|
/// El código del mercado al que pertenece el feriado (ej. "US", "BA").
|
||||||
|
/// </summary>
|
||||||
|
public string CodigoMercado { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La fecha exacta del feriado (la hora no es relevante).
|
||||||
|
/// </summary>
|
||||||
public DateTime Fecha { get; set; }
|
public DateTime Fecha { get; set; }
|
||||||
public string? Nombre { get; set; } // Nombre del feriado, si la API lo provee
|
|
||||||
|
/// <summary>
|
||||||
|
/// El nombre o la descripción del feriado (si está disponible).
|
||||||
|
/// </summary>
|
||||||
|
public string? Nombre { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ using FluentMigrator;
|
|||||||
|
|
||||||
namespace Mercados.Database.Migrations
|
namespace Mercados.Database.Migrations
|
||||||
{
|
{
|
||||||
// El número es la versión única de esta migración.
|
/// <summary>
|
||||||
// Usar un timestamp es una práctica común y segura.
|
/// Migración inicial que crea las tablas necesarias para almacenar
|
||||||
|
/// las cotizaciones de ganado, granos, bolsa y fuentes de datos.
|
||||||
|
/// </summary>
|
||||||
[Migration(20250701113000)]
|
[Migration(20250701113000)]
|
||||||
public class CreateInitialTables : Migration
|
public class CreateInitialTables : Migration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,15 +2,25 @@ using FluentMigrator;
|
|||||||
|
|
||||||
namespace Mercados.Database.Migrations
|
namespace Mercados.Database.Migrations
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Migración que añade la columna 'NombreEmpresa' a la tabla 'CotizacionesBolsa'
|
||||||
|
/// para almacenar el nombre descriptivo de la acción.
|
||||||
|
/// </summary>
|
||||||
[Migration(20250702133000)]
|
[Migration(20250702133000)]
|
||||||
public class AddNameToStocks : Migration
|
public class AddNameToStocks : Migration
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Aplica la migración, añadiendo la columna 'NombreEmpresa'.
|
||||||
|
/// </summary>
|
||||||
public override void Up()
|
public override void Up()
|
||||||
{
|
{
|
||||||
Alter.Table("CotizacionesBolsa")
|
Alter.Table("CotizacionesBolsa")
|
||||||
.AddColumn("NombreEmpresa").AsString(255).Nullable();
|
.AddColumn("NombreEmpresa").AsString(255).Nullable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Revierte la migración, eliminando la columna 'NombreEmpresa'.
|
||||||
|
/// </summary>
|
||||||
public override void Down()
|
public override void Down()
|
||||||
{
|
{
|
||||||
Delete.Column("NombreEmpresa").FromTable("CotizacionesBolsa");
|
Delete.Column("NombreEmpresa").FromTable("CotizacionesBolsa");
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ using FluentMigrator;
|
|||||||
|
|
||||||
namespace Mercados.Database.Migrations
|
namespace Mercados.Database.Migrations
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Migración para crear la tabla 'MercadosFeriados', que almacenará los días no laborables
|
||||||
|
/// para diferentes mercados bursátiles.
|
||||||
|
/// </summary>
|
||||||
[Migration(20250714150000)]
|
[Migration(20250714150000)]
|
||||||
public class CreateMercadoFeriadoTable : Migration
|
public class CreateMercadoFeriadoTable : Migration
|
||||||
{
|
{
|
||||||
private const string TableName = "MercadosFeriados";
|
private const string TableName = "MercadosFeriados";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Define la estructura de la tabla 'MercadosFeriados' y crea un índice único.
|
||||||
|
/// </summary>
|
||||||
public override void Up()
|
public override void Up()
|
||||||
{
|
{
|
||||||
Create.Table(TableName)
|
Create.Table(TableName)
|
||||||
@@ -23,6 +30,9 @@ namespace Mercados.Database.Migrations
|
|||||||
.WithOptions().Unique();
|
.WithOptions().Unique();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Revierte la migración eliminando la tabla 'MercadosFeriados'.
|
||||||
|
/// </summary>
|
||||||
public override void Down()
|
public override void Down()
|
||||||
{
|
{
|
||||||
Delete.Table(TableName);
|
Delete.Table(TableName);
|
||||||
|
|||||||
@@ -8,37 +8,83 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.DataFetchers
|
namespace Mercados.Infrastructure.DataFetchers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implementación de <see cref="IDataFetcher"/> para obtener datos de cotizaciones de granos
|
||||||
|
/// desde la API de la Bolsa de Comercio de Rosario (BCR).
|
||||||
|
/// </summary>
|
||||||
public class BcrDataFetcher : IDataFetcher
|
public class BcrDataFetcher : IDataFetcher
|
||||||
{
|
{
|
||||||
#region Clases DTO para la respuesta de la API de BCR
|
#region Clases DTO para la respuesta de la API de BCR
|
||||||
|
/// <summary>
|
||||||
|
/// DTO para la respuesta del endpoint de autenticación de BCR.
|
||||||
|
/// </summary>
|
||||||
private class BcrTokenResponse
|
private class BcrTokenResponse
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contenedor de datos del token.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("data")]
|
[JsonPropertyName("data")]
|
||||||
public TokenData? Data { get; set; }
|
public TokenData? Data { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contiene el token de autenticación.
|
||||||
|
/// </summary>
|
||||||
private class TokenData
|
private class TokenData
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// El token JWT para autenticar las solicitudes.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("token")]
|
[JsonPropertyName("token")]
|
||||||
public string? Token { get; set; }
|
public string? Token { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO para la respuesta del endpoint de precios de BCR.
|
||||||
|
/// </summary>
|
||||||
private class BcrPreciosResponse
|
private class BcrPreciosResponse
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lista de precios de granos.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("data")]
|
[JsonPropertyName("data")]
|
||||||
public List<BcrPrecioItem>? Data { get; set; }
|
public List<BcrPrecioItem>? Data { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Representa un ítem individual de precio en la respuesta de la API de BCR.
|
||||||
|
/// </summary>
|
||||||
private class BcrPrecioItem
|
private class BcrPrecioItem
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// El precio de cotización del grano.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("precio_Cotizacion")]
|
[JsonPropertyName("precio_Cotizacion")]
|
||||||
public decimal PrecioCotizacion { get; set; }
|
public decimal PrecioCotizacion { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// La variación del precio respecto a la cotización anterior.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("variacion_Precio_Cotizacion")]
|
[JsonPropertyName("variacion_Precio_Cotizacion")]
|
||||||
public decimal VariacionPrecioCotizacion { get; set; }
|
public decimal VariacionPrecioCotizacion { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// La fecha en que se realizó la operación.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("fecha_Operacion_Pizarra")]
|
[JsonPropertyName("fecha_Operacion_Pizarra")]
|
||||||
public DateTime FechaOperacionPizarra { get; set; }
|
public DateTime FechaOperacionPizarra { get; set; }
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public string SourceName => "BCR";
|
public string SourceName => "BCR";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL base de la API de BCR.
|
||||||
|
/// </summary>
|
||||||
private const string BaseUrl = "https://api.bcr.com.ar/gix/v1.0";
|
private const string BaseUrl = "https://api.bcr.com.ar/gix/v1.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mapeo de nombres de granos a sus IDs correspondientes en la API de BCR.
|
||||||
|
/// </summary>
|
||||||
private readonly Dictionary<string, int> _grainIds = new()
|
private readonly Dictionary<string, int> _grainIds = new()
|
||||||
{
|
{
|
||||||
{ "Trigo", 1 }, { "Maiz", 2 }, { "Sorgo", 3 }, { "Girasol", 20 }, { "Soja", 21 }
|
{ "Trigo", 1 }, { "Maiz", 2 }, { "Sorgo", 3 }, { "Girasol", 20 }, { "Soja", 21 }
|
||||||
@@ -50,6 +96,14 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<BcrDataFetcher> _logger;
|
private readonly ILogger<BcrDataFetcher> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="BcrDataFetcher"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClientFactory">Fábrica para crear instancias de HttpClient.</param>
|
||||||
|
/// <param name="cotizacionRepository">Repositorio para guardar las cotizaciones de granos.</param>
|
||||||
|
/// <param name="fuenteDatoRepository">Repositorio para gestionar la información de la fuente de datos.</param>
|
||||||
|
/// <param name="configuration">Configuración de la aplicación para acceder a las claves de API.</param>
|
||||||
|
/// <param name="logger">Logger para registrar información y errores.</param>
|
||||||
public BcrDataFetcher(
|
public BcrDataFetcher(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
ICotizacionGranoRepository cotizacionRepository,
|
ICotizacionGranoRepository cotizacionRepository,
|
||||||
@@ -64,6 +118,7 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<(bool Success, string Message)> FetchDataAsync()
|
public async Task<(bool Success, string Message)> FetchDataAsync()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
|
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
|
||||||
@@ -124,6 +179,11 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene un token de autenticación de la API de BCR.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">El cliente HTTP a utilizar para la solicitud.</param>
|
||||||
|
/// <returns>El token de autenticación como una cadena de texto, o null si la operación falla.</returns>
|
||||||
private async Task<string?> GetAuthTokenAsync(HttpClient client)
|
private async Task<string?> GetAuthTokenAsync(HttpClient client)
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/Login");
|
var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/Login");
|
||||||
@@ -137,6 +197,9 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
return tokenResponse?.Data?.Token;
|
return tokenResponse?.Data?.Token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Actualiza la información de la fuente de datos en la base de datos, registrando la última ejecución exitosa.
|
||||||
|
/// </summary>
|
||||||
private async Task UpdateSourceInfoAsync()
|
private async Task UpdateSourceInfoAsync()
|
||||||
{
|
{
|
||||||
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
|
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
|
||||||
|
|||||||
@@ -7,8 +7,18 @@ using System.Net.Http;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.DataFetchers
|
namespace Mercados.Infrastructure.DataFetchers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implementación de <see cref="IDataFetcher"/> para obtener datos de cotizaciones de bolsa
|
||||||
|
/// desde la API de Finnhub.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Utiliza la librería ThreeFourteen.Finnhub.Client para interactuar con la API.
|
||||||
|
/// </remarks>
|
||||||
public class FinnhubDataFetcher : IDataFetcher
|
public class FinnhubDataFetcher : IDataFetcher
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Nombre de la fuente de datos utilizada por este fetcher.
|
||||||
|
/// </summary>
|
||||||
public string SourceName => "Finnhub";
|
public string SourceName => "Finnhub";
|
||||||
private readonly List<string> _tickers = new() {
|
private readonly List<string> _tickers = new() {
|
||||||
// Tecnológicas y ETFs
|
// Tecnológicas y ETFs
|
||||||
@@ -24,6 +34,17 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
private readonly IFuenteDatoRepository _fuenteDatoRepository;
|
private readonly IFuenteDatoRepository _fuenteDatoRepository;
|
||||||
private readonly ILogger<FinnhubDataFetcher> _logger;
|
private readonly ILogger<FinnhubDataFetcher> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="FinnhubDataFetcher"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">Configuración de la aplicación para acceder a la clave de API de Finnhub.</param>
|
||||||
|
/// <param name="httpClientFactory">Fábrica para crear instancias de HttpClient.</param>
|
||||||
|
/// <param name="cotizacionRepository">Repositorio para guardar las cotizaciones de bolsa obtenidas.</param>
|
||||||
|
/// <param name="fuenteDatoRepository">Repositorio para gestionar la información de la fuente de datos (Finnhub).</param>
|
||||||
|
/// <param name="logger">Logger para registrar información y errores durante la ejecución.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// Se lanza si la clave de API de Finnhub no está configurada en la aplicación.
|
||||||
|
/// </exception>
|
||||||
public FinnhubDataFetcher(
|
public FinnhubDataFetcher(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
@@ -42,6 +63,12 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene los datos de cotizaciones de bolsa desde la API de Finnhub para los tickers configurados
|
||||||
|
/// y los guarda en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado.</returns>
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<(bool Success, string Message)> FetchDataAsync()
|
public async Task<(bool Success, string Message)> FetchDataAsync()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
|
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
|
||||||
@@ -88,6 +115,9 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros.");
|
return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Actualiza la información de la fuente de datos (Finnhub) en la base de datos.
|
||||||
|
/// </summary>
|
||||||
private async Task UpdateSourceInfoAsync()
|
private async Task UpdateSourceInfoAsync()
|
||||||
{
|
{
|
||||||
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
|
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
|
||||||
|
|||||||
@@ -7,24 +7,51 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.DataFetchers
|
namespace Mercados.Infrastructure.DataFetchers
|
||||||
{
|
{
|
||||||
// Añadimos las clases DTO necesarias para deserializar la respuesta de Finnhub
|
/// <summary>
|
||||||
|
/// DTO para deserializar la respuesta de la API de Finnhub al obtener feriados de mercado.
|
||||||
|
/// </summary>
|
||||||
public class MarketHolidayResponse
|
public class MarketHolidayResponse
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lista de feriados del mercado.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("data")]
|
[JsonPropertyName("data")]
|
||||||
public List<MarketHoliday>? Data { get; set; }
|
public List<MarketHoliday>? Data { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Representa un feriado de mercado individual en la respuesta de la API de Finnhub.
|
||||||
|
/// </summary>
|
||||||
public class MarketHoliday
|
public class MarketHoliday
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fecha del feriado en formato de cadena (YYYY-MM-DD).
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("at")]
|
[JsonPropertyName("at")]
|
||||||
public string? At { get; set; }
|
public string? At { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fecha del feriado como <see cref="DateOnly"/>.
|
||||||
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public DateOnly Date => DateOnly.FromDateTime(DateTime.Parse(At!));
|
public DateOnly Date => DateOnly.FromDateTime(DateTime.Parse(At!));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementación de <see cref="IDataFetcher"/> para obtener datos de feriados de mercado
|
||||||
|
/// desde la API de Finnhub.
|
||||||
|
/// </summary>
|
||||||
public class HolidayDataFetcher : IDataFetcher
|
public class HolidayDataFetcher : IDataFetcher
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public string SourceName => "Holidays";
|
public string SourceName => "Holidays";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Códigos de mercado para los cuales se obtendrán los feriados.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// "US" para Estados Unidos, "BA" para Argentina (Bolsa de Comercio de Buenos Aires).
|
||||||
|
/// </remarks>
|
||||||
private readonly string[] _marketCodes = { "US", "BA" };
|
private readonly string[] _marketCodes = { "US", "BA" };
|
||||||
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
@@ -32,6 +59,16 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<HolidayDataFetcher> _logger;
|
private readonly ILogger<HolidayDataFetcher> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="HolidayDataFetcher"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClientFactory">Fábrica para crear instancias de HttpClient.</param>
|
||||||
|
/// <param name="feriadoRepository">Repositorio para gestionar los feriados de mercado en la base de datos.</param>
|
||||||
|
/// <param name="configuration">Configuración de la aplicación para acceder a la clave de API de Finnhub.</param>
|
||||||
|
/// <param name="logger">Logger para registrar información y errores durante la ejecución.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// Se lanza si la clave de API de Finnhub no está configurada en la aplicación.
|
||||||
|
/// </exception>
|
||||||
public HolidayDataFetcher(
|
public HolidayDataFetcher(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IMercadoFeriadoRepository feriadoRepository,
|
IMercadoFeriadoRepository feriadoRepository,
|
||||||
@@ -44,14 +81,24 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene los datos de feriados de mercado desde la API de Finnhub y los guarda en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado.</returns>
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<(bool Success, string Message)> FetchDataAsync()
|
public async Task<(bool Success, string Message)> FetchDataAsync()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Iniciando actualización de feriados.");
|
_logger.LogInformation("Iniciando actualización de feriados.");
|
||||||
|
|
||||||
|
// Verificamos que la API Key de Finnhub esté configurada
|
||||||
var apiKey = _configuration["ApiKeys:Finnhub"];
|
var apiKey = _configuration["ApiKeys:Finnhub"];
|
||||||
if (string.IsNullOrEmpty(apiKey)) return (false, "API Key de Finnhub no configurada.");
|
if (string.IsNullOrEmpty(apiKey))
|
||||||
|
{
|
||||||
|
return (false, "API Key de Finnhub no configurada.");
|
||||||
|
}
|
||||||
|
|
||||||
var client = _httpClientFactory.CreateClient("FinnhubDataFetcher");
|
var client = _httpClientFactory.CreateClient("FinnhubDataFetcher");
|
||||||
|
// Iteramos sobre cada código de mercado configurado
|
||||||
foreach (var marketCode in _marketCodes)
|
foreach (var marketCode in _marketCodes)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -59,18 +106,24 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
var apiUrl = $"https://finnhub.io/api/v1/stock/market-holiday?exchange={marketCode}&token={apiKey}";
|
var apiUrl = $"https://finnhub.io/api/v1/stock/market-holiday?exchange={marketCode}&token={apiKey}";
|
||||||
// Ahora la deserialización funcionará porque la clase existe
|
// Ahora la deserialización funcionará porque la clase existe
|
||||||
var response = await client.GetFromJsonAsync<MarketHolidayResponse>(apiUrl);
|
var response = await client.GetFromJsonAsync<MarketHolidayResponse>(apiUrl);
|
||||||
|
|
||||||
|
// Si obtuvimos datos en la respuesta
|
||||||
if (response?.Data != null)
|
if (response?.Data != null)
|
||||||
{
|
{
|
||||||
|
// Convertimos los datos de la API al formato de nuestra entidad MercadoFeriado
|
||||||
var nuevosFeriados = response.Data.Select(h => new MercadoFeriado
|
var nuevosFeriados = response.Data.Select(h => new MercadoFeriado
|
||||||
{
|
{
|
||||||
CodigoMercado = marketCode,
|
CodigoMercado = marketCode,
|
||||||
Fecha = h.Date.ToDateTime(TimeOnly.MinValue),
|
Fecha = h.Date.ToDateTime(TimeOnly.MinValue),
|
||||||
Nombre = "Feriado Bursátil"
|
Nombre = "Feriado Bursátil"
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
// Guardamos los feriados en la base de datos, reemplazando los existentes para ese mercado
|
||||||
await _feriadoRepository.ReemplazarFeriadosPorMercadoAsync(marketCode, nuevosFeriados);
|
await _feriadoRepository.ReemplazarFeriadosPorMercadoAsync(marketCode, nuevosFeriados);
|
||||||
_logger.LogInformation("Feriados para {MarketCode} actualizados exitosamente: {Count} registros.", marketCode, nuevosFeriados.Count);
|
_logger.LogInformation(
|
||||||
|
"Feriados para {MarketCode} actualizados exitosamente: {Count} registros.",
|
||||||
|
marketCode,
|
||||||
|
nuevosFeriados.Count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -78,6 +131,7 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
_logger.LogError(ex, "Falló la obtención de feriados para {MarketCode}.", marketCode);
|
_logger.LogError(ex, "Falló la obtención de feriados para {MarketCode}.", marketCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Retornamos éxito si el proceso completo se ejecutó sin errores irrecuperables
|
||||||
return (true, "Actualización de feriados completada.");
|
return (true, "Actualización de feriados completada.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,39 @@ using System.Globalization;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.DataFetchers
|
namespace Mercados.Infrastructure.DataFetchers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implementación de <see cref="IDataFetcher"/> para obtener datos de cotizaciones de ganado
|
||||||
|
/// desde el sitio web de Mercado Agro Ganadero.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Utiliza AngleSharp para el parsing del HTML.
|
||||||
|
/// </remarks>
|
||||||
public class MercadoAgroFetcher : IDataFetcher
|
public class MercadoAgroFetcher : IDataFetcher
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public string SourceName => "MercadoAgroganadero";
|
public string SourceName => "MercadoAgroganadero";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL del sitio web de Mercado Agro Ganadero donde se encuentran las cotizaciones.
|
||||||
|
/// </summary>
|
||||||
private const string DataUrl = "https://www.mercadoagroganadero.com.ar/dll/hacienda6.dll/haciinfo000225";
|
private const string DataUrl = "https://www.mercadoagroganadero.com.ar/dll/hacienda6.dll/haciinfo000225";
|
||||||
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly ICotizacionGanadoRepository _cotizacionRepository;
|
private readonly ICotizacionGanadoRepository _cotizacionRepository;
|
||||||
private readonly IFuenteDatoRepository _fuenteDatoRepository;
|
private readonly IFuenteDatoRepository _fuenteDatoRepository;
|
||||||
private readonly ILogger<MercadoAgroFetcher> _logger;
|
private readonly ILogger<MercadoAgroFetcher> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="MercadoAgroFetcher"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClientFactory">Fábrica para crear instancias de HttpClient, configuradas con políticas de reintento.</param>
|
||||||
|
/// <param name="cotizacionRepository">Repositorio para guardar las cotizaciones de ganado obtenidas.</param>
|
||||||
|
/// <param name="fuenteDatoRepository">Repositorio para gestionar la información de la fuente de datos (Mercado Agro Ganadero).</param>
|
||||||
|
/// <param name="logger">Logger para registrar información y errores durante la ejecución.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// El constructor requiere una <see cref="IHttpClientFactory"/> que debe tener configurado un cliente HTTP
|
||||||
|
/// con el nombre "MercadoAgroFetcher", y este cliente debe tener aplicada una política de reintentos (ej. con Polly).
|
||||||
|
/// </remarks>
|
||||||
public MercadoAgroFetcher(
|
public MercadoAgroFetcher(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
ICotizacionGanadoRepository cotizacionRepository,
|
ICotizacionGanadoRepository cotizacionRepository,
|
||||||
@@ -27,6 +51,12 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene los datos de cotizaciones de ganado desde el sitio web de Mercado Agro Ganadero,
|
||||||
|
/// los parsea y los guarda en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado.</returns>
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<(bool Success, string Message)> FetchDataAsync()
|
public async Task<(bool Success, string Message)> FetchDataAsync()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
|
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
|
||||||
@@ -36,15 +66,12 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
var htmlContent = await GetHtmlContentAsync();
|
var htmlContent = await GetHtmlContentAsync();
|
||||||
if (string.IsNullOrEmpty(htmlContent))
|
if (string.IsNullOrEmpty(htmlContent))
|
||||||
{
|
{
|
||||||
// Esto sigue siendo un fallo, no se pudo obtener la página
|
|
||||||
return (false, "No se pudo obtener el contenido HTML.");
|
return (false, "No se pudo obtener el contenido HTML.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var cotizaciones = ParseHtmlToEntities(htmlContent);
|
var cotizaciones = ParseHtmlToEntities(htmlContent);
|
||||||
|
|
||||||
if (!cotizaciones.Any())
|
if (!cotizaciones.Any())
|
||||||
{
|
{
|
||||||
// La conexión fue exitosa, pero no se encontraron datos válidos.
|
|
||||||
// Esto NO es un error crítico, es un estado informativo.
|
// Esto NO es un error crítico, es un estado informativo.
|
||||||
_logger.LogInformation("La conexión con {SourceName} fue exitosa, pero no se encontraron datos de cotizaciones para procesar.", SourceName);
|
_logger.LogInformation("La conexión con {SourceName} fue exitosa, pero no se encontraron datos de cotizaciones para procesar.", SourceName);
|
||||||
return (true, "Conexión exitosa, pero no se encontraron nuevos datos.");
|
return (true, "Conexión exitosa, pero no se encontraron nuevos datos.");
|
||||||
@@ -64,21 +91,34 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene el contenido HTML de la página de cotizaciones.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>El contenido HTML como una cadena.</returns>
|
||||||
|
/// <exception cref="HttpRequestException">
|
||||||
|
/// Se lanza si la solicitud HTTP no es exitosa.
|
||||||
|
/// </exception>
|
||||||
private async Task<string> GetHtmlContentAsync()
|
private async Task<string> GetHtmlContentAsync()
|
||||||
{
|
{
|
||||||
// Pedimos el cliente HTTP con el nombre específico que tiene la política de Polly
|
// Pedimos el cliente HTTP con el nombre específico que tiene la política de Polly
|
||||||
var client = _httpClientFactory.CreateClient("MercadoAgroFetcher");
|
var client = _httpClientFactory.CreateClient("MercadoAgroFetcher");
|
||||||
|
|
||||||
// Es importante simular un navegador para evitar bloqueos.
|
// Es importante simular un navegador para evitar bloqueos.
|
||||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
|
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
|
||||||
|
|
||||||
var response = await client.GetAsync(DataUrl);
|
var response = await client.GetAsync(DataUrl);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
// El sitio usa una codificación específica, hay que decodificarla correctamente.
|
// El sitio usa una codificación específica, hay que decodificarla correctamente.
|
||||||
var stream = await response.Content.ReadAsStreamAsync();
|
var stream = await response.Content.ReadAsStreamAsync();
|
||||||
using var reader = new StreamReader(stream, System.Text.Encoding.GetEncoding("windows-1252"));
|
using var reader = new StreamReader(stream, System.Text.Encoding.GetEncoding("windows-1252"));
|
||||||
return await reader.ReadToEndAsync();
|
return await reader.ReadToEndAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parsea el contenido HTML para extraer las cotizaciones de ganado.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="html">El HTML a parsear.</param>
|
||||||
|
/// <returns>Una lista de entidades <see cref="CotizacionGanado"/>.</returns>
|
||||||
private List<CotizacionGanado> ParseHtmlToEntities(string html)
|
private List<CotizacionGanado> ParseHtmlToEntities(string html)
|
||||||
{
|
{
|
||||||
var config = Configuration.Default;
|
var config = Configuration.Default;
|
||||||
@@ -109,7 +149,7 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
Categoria = celdas[1],
|
Categoria = celdas[1],
|
||||||
Especificaciones = $"{celdas[2]} - {celdas[3]}",
|
Especificaciones = $"{celdas[2]} - {celdas[3]}",
|
||||||
Maximo = ParseDecimal(celdas[4]),
|
Maximo = ParseDecimal(celdas[4]),
|
||||||
Minimo = ParseDecimal(celdas[5]),
|
Minimo = ParseDecimal(celdas[5]),
|
||||||
Promedio = ParseDecimal(celdas[6]),
|
Promedio = ParseDecimal(celdas[6]),
|
||||||
Mediano = ParseDecimal(celdas[7]),
|
Mediano = ParseDecimal(celdas[7]),
|
||||||
Cabezas = ParseInt(celdas[8]),
|
Cabezas = ParseInt(celdas[8]),
|
||||||
@@ -122,21 +162,24 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "No se pudo parsear una fila de la tabla. Contenido: {RowContent}", string.Join(" | ", celdas));
|
_logger.LogWarning(ex, "No se pudo parsear una fila de la tabla. Contenido: {RowContent}",
|
||||||
|
string.Join(" | ", celdas));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cotizaciones;
|
return cotizaciones;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Actualiza la información de la fuente de datos (Mercado Agro Ganadero) en la base de datos.
|
||||||
|
/// </summary>
|
||||||
private async Task UpdateSourceInfoAsync()
|
private async Task UpdateSourceInfoAsync()
|
||||||
{
|
{
|
||||||
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
|
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
|
||||||
if (fuente == null)
|
if (fuente == null)
|
||||||
{
|
{
|
||||||
await _fuenteDatoRepository.CrearAsync(new FuenteDato
|
await _fuenteDatoRepository.CrearAsync(new FuenteDato
|
||||||
{
|
{
|
||||||
Nombre = SourceName,
|
Nombre = SourceName,
|
||||||
Url = DataUrl,
|
Url = DataUrl,
|
||||||
UltimaEjecucionExitosa = DateTime.UtcNow
|
UltimaEjecucionExitosa = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
@@ -150,17 +193,33 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Funciones de Ayuda para Parseo ---
|
// --- Funciones de Ayuda para Parseo ---
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="CultureInfo"/> para el parseo de números en formato "es-AR".
|
||||||
|
/// </summary>
|
||||||
private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR");
|
private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parsea una cadena a decimal, considerando el formato numérico de Argentina.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">La cadena a parsear.</param>
|
||||||
|
/// <returns>El valor decimal parseado.</returns>
|
||||||
private decimal ParseDecimal(string value)
|
private decimal ParseDecimal(string value)
|
||||||
{
|
{
|
||||||
// El sitio usa '.' como separador de miles y ',' como decimal.
|
// El sitio usa '.' como separador de miles y ',' como decimal.
|
||||||
// Primero quitamos el de miles, luego reemplazamos la coma decimal por un punto.
|
// Primero quitamos el de miles, luego reemplazamos la coma decimal por un punto.
|
||||||
var cleanValue = value.Replace("$", "").Replace(".", "").Trim();
|
var cleanValue = value.Replace("$", "").Replace(".", "").Trim();
|
||||||
return decimal.Parse(cleanValue, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, _cultureInfo);
|
return decimal.Parse(cleanValue, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, _cultureInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parsea una cadena a entero, quitando separadores de miles.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">La cadena a parsear.</param>
|
||||||
|
/// <returns>El valor entero parseado.</returns>
|
||||||
private int ParseInt(string value)
|
private int ParseInt(string value)
|
||||||
{
|
{
|
||||||
return int.Parse(value.Replace(".", ""), CultureInfo.InvariantCulture);
|
return int.Parse(value.Replace(".", ""), CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
namespace Mercados.Infrastructure.DataFetchers
|
namespace Mercados.Infrastructure.DataFetchers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Clase estática que proporciona un mapeo entre los tickers de acciones y sus nombres descriptivos.
|
||||||
|
/// </summary>
|
||||||
public static class TickerNameMapping
|
public static class TickerNameMapping
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Diccionario privado que almacena los tickers como claves y los nombres de las empresas como valores.
|
||||||
|
/// La comparación de claves no distingue entre mayúsculas y minúsculas.
|
||||||
|
/// </summary>
|
||||||
private static readonly Dictionary<string, string> Names = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly Dictionary<string, string> Names = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
// USA
|
// USA
|
||||||
{ "SPY", "S&P 500 ETF" }, // Cambiado de GSPC a SPY para Finnhub
|
{ "SPY", "S&P 500 ETF" },
|
||||||
{ "AAPL", "Apple Inc." },
|
{ "AAPL", "Apple Inc." },
|
||||||
{ "MSFT", "Microsoft Corp." },
|
|
||||||
{ "AMZN", "Amazon.com, Inc." },
|
{ "AMZN", "Amazon.com, Inc." },
|
||||||
{ "NVDA", "NVIDIA Corp." },
|
{ "NVDA", "NVIDIA Corp." },
|
||||||
{ "AMD", "Advanced Micro Devices" },
|
{ "AMD", "Advanced Micro Devices" },
|
||||||
@@ -19,6 +25,7 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
{ "XLE", "Energy Select Sector SPDR" },
|
{ "XLE", "Energy Select Sector SPDR" },
|
||||||
{ "XLK", "Technology Select Sector SPDR" },
|
{ "XLK", "Technology Select Sector SPDR" },
|
||||||
{ "MELI", "MercadoLibre, Inc." },
|
{ "MELI", "MercadoLibre, Inc." },
|
||||||
|
{ "MSFT", "Microsoft Corp." },
|
||||||
{ "GLOB", "Globant" },
|
{ "GLOB", "Globant" },
|
||||||
|
|
||||||
// ADRs Argentinos que cotizan en EEUU
|
// ADRs Argentinos que cotizan en EEUU
|
||||||
@@ -53,9 +60,15 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
{ "MELI.BA", "MercadoLibre (CEDEAR)" }, // Aclaramos que es el CEDEAR
|
{ "MELI.BA", "MercadoLibre (CEDEAR)" }, // Aclaramos que es el CEDEAR
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene el nombre descriptivo asociado a un ticker.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ticker">El ticker de la acción (ej. "AAPL").</param>
|
||||||
|
/// <returns>El nombre completo de la empresa si se encuentra en el mapeo; de lo contrario, null.</returns>
|
||||||
public static string? GetName(string ticker)
|
public static string? GetName(string ticker)
|
||||||
{
|
{
|
||||||
return Names.GetValueOrDefault(ticker);
|
// Devuelve el nombre si existe, o null si no se encuentra la clave.
|
||||||
|
return Names.TryGetValue(ticker, out var name) ? name : $"Ticker no reconocido: {ticker}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,24 @@ using YahooFinanceApi;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.DataFetchers
|
namespace Mercados.Infrastructure.DataFetchers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implementación de <see cref="IDataFetcher"/> para obtener datos de cotizaciones de bolsa
|
||||||
|
/// desde la API de Yahoo Finance.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Utiliza la librería YahooFinanceApi para interactuar con la API.
|
||||||
|
/// </remarks>
|
||||||
public class YahooFinanceDataFetcher : IDataFetcher
|
public class YahooFinanceDataFetcher : IDataFetcher
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public string SourceName => "YahooFinance";
|
public string SourceName => "YahooFinance";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lista de tickers a obtener de Yahoo Finance.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Incluye el índice S&P 500, acciones del Merval argentino y algunos CEDEARs.
|
||||||
|
/// </remarks>
|
||||||
private readonly List<string> _tickers = new() {
|
private readonly List<string> _tickers = new() {
|
||||||
"^GSPC", // Índice S&P 500
|
"^GSPC", // Índice S&P 500
|
||||||
"^MERV", "GGAL.BA", "YPFD.BA", "PAMP.BA", "BMA.BA", "COME.BA",
|
"^MERV", "GGAL.BA", "YPFD.BA", "PAMP.BA", "BMA.BA", "COME.BA",
|
||||||
@@ -15,20 +30,36 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
"CEPU.BA", "LOMA.BA", "VALO.BA", "MELI.BA"
|
"CEPU.BA", "LOMA.BA", "VALO.BA", "MELI.BA"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diccionario para almacenar el mapeo de tickers con su información de mercado (Local o EEUU).
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<string, string> _tickerMarketMapping = new Dictionary<string, string>();
|
||||||
|
|
||||||
private readonly ICotizacionBolsaRepository _cotizacionRepository;
|
private readonly ICotizacionBolsaRepository _cotizacionRepository;
|
||||||
private readonly IFuenteDatoRepository _fuenteDatoRepository;
|
private readonly IFuenteDatoRepository _fuenteDatoRepository;
|
||||||
private readonly ILogger<YahooFinanceDataFetcher> _logger;
|
private readonly ILogger<YahooFinanceDataFetcher> _logger;
|
||||||
|
|
||||||
public YahooFinanceDataFetcher(
|
/// <summary>
|
||||||
ICotizacionBolsaRepository cotizacionRepository,
|
/// Inicializa una nueva instancia de la clase <see cref="YahooFinanceDataFetcher"/>.
|
||||||
IFuenteDatoRepository fuenteDatoRepository,
|
/// </summary>
|
||||||
ILogger<YahooFinanceDataFetcher> logger)
|
/// <param name="cotizacionRepository">Repositorio para guardar las cotizaciones de bolsa obtenidas.</param>
|
||||||
{
|
/// <param name="fuenteDatoRepository">Repositorio para gestionar la información de la fuente de datos (Yahoo Finance).</param>
|
||||||
_cotizacionRepository = cotizacionRepository;
|
/// <param name="logger">Logger para registrar información y errores durante la ejecución.</param>
|
||||||
_fuenteDatoRepository = fuenteDatoRepository;
|
public YahooFinanceDataFetcher(
|
||||||
_logger = logger;
|
ICotizacionBolsaRepository cotizacionRepository,
|
||||||
}
|
IFuenteDatoRepository fuenteDatoRepository,
|
||||||
|
ILogger<YahooFinanceDataFetcher> logger)
|
||||||
|
{
|
||||||
|
_cotizacionRepository = cotizacionRepository;
|
||||||
|
_fuenteDatoRepository = fuenteDatoRepository;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene los datos de cotizaciones de bolsa desde la API de Yahoo Finance para los tickers configurados
|
||||||
|
/// y los guarda en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado.</returns>
|
||||||
public async Task<(bool Success, string Message)> FetchDataAsync()
|
public async Task<(bool Success, string Message)> FetchDataAsync()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
|
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
|
||||||
@@ -41,7 +72,7 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
{
|
{
|
||||||
if (sec.RegularMarketPrice == 0 || sec.RegularMarketPreviousClose == 0) continue;
|
if (sec.RegularMarketPrice == 0 || sec.RegularMarketPreviousClose == 0) continue;
|
||||||
|
|
||||||
string mercado = sec.Symbol.EndsWith(".BA") || sec.Symbol == "^MERV" ? "Local" : "EEUU";
|
string mercado = DetermineMarket(sec.Symbol);
|
||||||
|
|
||||||
cotizaciones.Add(new CotizacionBolsa
|
cotizaciones.Add(new CotizacionBolsa
|
||||||
{
|
{
|
||||||
@@ -75,6 +106,27 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determina el mercado (Local o EEUU) para un ticker específico.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">El ticker de la acción.</param>
|
||||||
|
/// <returns>El mercado al que pertenece el ticker.</returns>
|
||||||
|
private string DetermineMarket(string symbol)
|
||||||
|
{
|
||||||
|
if (_tickerMarketMapping.TryGetValue(symbol, out string? market))
|
||||||
|
{
|
||||||
|
return market;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no existe en el mapping, determinamos y lo agregamos.
|
||||||
|
market = symbol.EndsWith(".BA") || symbol == "^MERV" ? "Local" : "EEUU";
|
||||||
|
_tickerMarketMapping[symbol] = market;
|
||||||
|
return market;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Actualiza la información de la fuente de datos (Yahoo Finance) en la base de datos.
|
||||||
|
/// </summary>
|
||||||
private async Task UpdateSourceInfoAsync()
|
private async Task UpdateSourceInfoAsync()
|
||||||
{
|
{
|
||||||
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
|
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ using System.Data;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence
|
namespace Mercados.Infrastructure.Persistence
|
||||||
{
|
{
|
||||||
public interface IDbConnectionFactory
|
/// <summary>
|
||||||
{
|
/// Define una interfaz para una fábrica de conexiones a la base de datos.
|
||||||
IDbConnection CreateConnection();
|
/// </summary>
|
||||||
}
|
public interface IDbConnectionFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Crea y abre una nueva conexión a la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Un objeto <see cref="IDbConnection"/> representando la conexión abierta.</returns>
|
||||||
|
IDbConnection CreateConnection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,26 +4,34 @@ using System.Data;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="ICotizacionBolsaRepository"/>
|
||||||
public class CotizacionBolsaRepository : ICotizacionBolsaRepository
|
public class CotizacionBolsaRepository : ICotizacionBolsaRepository
|
||||||
{
|
{
|
||||||
private readonly IDbConnectionFactory _connectionFactory;
|
private readonly IDbConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="CotizacionBolsaRepository"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connectionFactory">Fábrica para crear conexiones a la base de datos.</param>
|
||||||
public CotizacionBolsaRepository(IDbConnectionFactory connectionFactory)
|
public CotizacionBolsaRepository(IDbConnectionFactory connectionFactory)
|
||||||
{
|
{
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task GuardarMuchosAsync(IEnumerable<CotizacionBolsa> cotizaciones)
|
public async Task GuardarMuchosAsync(IEnumerable<CotizacionBolsa> cotizaciones)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
|
|
||||||
const string sql = @"
|
const string sql = @"INSERT INTO
|
||||||
INSERT INTO CotizacionesBolsa (Ticker, NombreEmpresa, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro)
|
CotizacionesBolsa (Ticker, NombreEmpresa, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro)
|
||||||
VALUES (@Ticker, @NombreEmpresa, @Mercado, @PrecioActual, @Apertura, @CierreAnterior, @PorcentajeCambio, @FechaRegistro);";
|
VALUES
|
||||||
|
(@Ticker, @NombreEmpresa, @Mercado, @PrecioActual, @Apertura, @CierreAnterior, @PorcentajeCambio, @FechaRegistro);";
|
||||||
|
|
||||||
await connection.ExecuteAsync(sql, cotizaciones);
|
await connection.ExecuteAsync(sql, cotizaciones);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<CotizacionBolsa>> ObtenerUltimasPorMercadoAsync(string mercado)
|
public async Task<IEnumerable<CotizacionBolsa>> ObtenerUltimasPorMercadoAsync(string mercado)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
@@ -48,6 +56,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories
|
|||||||
return await connection.QueryAsync<CotizacionBolsa>(sql, new { Mercado = mercado });
|
return await connection.QueryAsync<CotizacionBolsa>(sql, new { Mercado = mercado });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<CotizacionBolsa>> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias)
|
public async Task<IEnumerable<CotizacionBolsa>> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
|
|||||||
@@ -4,22 +4,29 @@ using System.Data;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="ICotizacionGanadoRepository"/>
|
||||||
public class CotizacionGanadoRepository : ICotizacionGanadoRepository
|
public class CotizacionGanadoRepository : ICotizacionGanadoRepository
|
||||||
{
|
{
|
||||||
private readonly IDbConnectionFactory _connectionFactory;
|
private readonly IDbConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="CotizacionGanadoRepository"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connectionFactory">Fábrica para crear conexiones a la base de datos.</param>
|
||||||
public CotizacionGanadoRepository(IDbConnectionFactory connectionFactory)
|
public CotizacionGanadoRepository(IDbConnectionFactory connectionFactory)
|
||||||
{
|
{
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task GuardarMuchosAsync(IEnumerable<CotizacionGanado> cotizaciones)
|
public async Task GuardarMuchosAsync(IEnumerable<CotizacionGanado> cotizaciones)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
|
|
||||||
// Dapper puede insertar una colección de objetos de una sola vez, ¡muy eficiente!
|
// Dapper puede insertar una colección de objetos de una sola vez, ¡muy eficiente!
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
INSERT INTO CotizacionesGanado (
|
INSERT INTO
|
||||||
|
CotizacionesGanado (
|
||||||
Categoria, Especificaciones, Maximo, Minimo, Promedio, Mediano,
|
Categoria, Especificaciones, Maximo, Minimo, Promedio, Mediano,
|
||||||
Cabezas, KilosTotales, KilosPorCabeza, ImporteTotal, FechaRegistro
|
Cabezas, KilosTotales, KilosPorCabeza, ImporteTotal, FechaRegistro
|
||||||
)
|
)
|
||||||
@@ -30,6 +37,8 @@ namespace Mercados.Infrastructure.Persistence.Repositories
|
|||||||
|
|
||||||
await connection.ExecuteAsync(sql, cotizaciones);
|
await connection.ExecuteAsync(sql, cotizaciones);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<CotizacionGanado>> ObtenerUltimaTandaAsync()
|
public async Task<IEnumerable<CotizacionGanado>> ObtenerUltimaTandaAsync()
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
@@ -44,6 +53,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories
|
|||||||
return await connection.QueryAsync<CotizacionGanado>(sql);
|
return await connection.QueryAsync<CotizacionGanado>(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<CotizacionGanado>> ObtenerHistorialAsync(string categoria, string especificaciones, int dias)
|
public async Task<IEnumerable<CotizacionGanado>> ObtenerHistorialAsync(string categoria, string especificaciones, int dias)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
|
|||||||
@@ -4,25 +4,34 @@ using System.Data;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="ICotizacionGranoRepository"/>
|
||||||
public class CotizacionGranoRepository : ICotizacionGranoRepository
|
public class CotizacionGranoRepository : ICotizacionGranoRepository
|
||||||
{
|
{
|
||||||
private readonly IDbConnectionFactory _connectionFactory;
|
private readonly IDbConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="CotizacionGranoRepository"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connectionFactory">Fábrica para crear conexiones a la base de datos.</param>
|
||||||
public CotizacionGranoRepository(IDbConnectionFactory connectionFactory)
|
public CotizacionGranoRepository(IDbConnectionFactory connectionFactory)
|
||||||
{
|
{
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task GuardarMuchosAsync(IEnumerable<CotizacionGrano> cotizaciones)
|
public async Task GuardarMuchosAsync(IEnumerable<CotizacionGrano> cotizaciones)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
|
|
||||||
const string sql = @"
|
const string sql = @"INSERT INTO
|
||||||
INSERT INTO CotizacionesGranos (Nombre, Precio, VariacionPrecio, FechaOperacion, FechaRegistro)
|
CotizacionesGranos (Nombre, Precio, VariacionPrecio, FechaOperacion, FechaRegistro)
|
||||||
VALUES (@Nombre, @Precio, @VariacionPrecio, @FechaOperacion, @FechaRegistro);";
|
VALUES
|
||||||
|
(@Nombre, @Precio, @VariacionPrecio, @FechaOperacion, @FechaRegistro);";
|
||||||
|
|
||||||
await connection.ExecuteAsync(sql, cotizaciones);
|
await connection.ExecuteAsync(sql, cotizaciones);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<CotizacionGrano>> ObtenerUltimasAsync()
|
public async Task<IEnumerable<CotizacionGrano>> ObtenerUltimasAsync()
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
@@ -45,6 +54,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories
|
|||||||
return await connection.QueryAsync<CotizacionGrano>(sql);
|
return await connection.QueryAsync<CotizacionGrano>(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<CotizacionGrano>> ObtenerHistorialAsync(string nombre, int dias)
|
public async Task<IEnumerable<CotizacionGrano>> ObtenerHistorialAsync(string nombre, int dias)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
|
|||||||
@@ -4,37 +4,42 @@ using System.Data;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="IFuenteDatoRepository"/>
|
||||||
public class FuenteDatoRepository : IFuenteDatoRepository
|
public class FuenteDatoRepository : IFuenteDatoRepository
|
||||||
{
|
{
|
||||||
private readonly IDbConnectionFactory _connectionFactory;
|
private readonly IDbConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="FuenteDatoRepository"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connectionFactory">Fábrica para crear conexiones a la base de datos.</param>
|
||||||
public FuenteDatoRepository(IDbConnectionFactory connectionFactory)
|
public FuenteDatoRepository(IDbConnectionFactory connectionFactory)
|
||||||
{
|
{
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<FuenteDato?> ObtenerPorNombreAsync(string nombre)
|
public async Task<FuenteDato?> ObtenerPorNombreAsync(string nombre)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
const string sql = "SELECT * FROM FuentesDatos WHERE Nombre = @Nombre;";
|
const string sql = "SELECT * FROM FuentesDatos WHERE Nombre = @Nombre;";
|
||||||
return await connection.QuerySingleOrDefaultAsync<FuenteDato>(sql, new { Nombre = nombre });
|
return await connection.QuerySingleOrDefaultAsync<FuenteDato>(sql, new { Nombre = nombre });
|
||||||
}
|
}
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task CrearAsync(FuenteDato fuenteDato)
|
public async Task CrearAsync(FuenteDato fuenteDato)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
const string sql = @"
|
const string sql = @"INSERT INTO FuentesDatos (Nombre, UltimaEjecucionExitosa, Url)
|
||||||
INSERT INTO FuentesDatos (Nombre, UltimaEjecucionExitosa, Url)
|
|
||||||
VALUES (@Nombre, @UltimaEjecucionExitosa, @Url);";
|
VALUES (@Nombre, @UltimaEjecucionExitosa, @Url);";
|
||||||
await connection.ExecuteAsync(sql, fuenteDato);
|
await connection.ExecuteAsync(sql, fuenteDato);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task ActualizarAsync(FuenteDato fuenteDato)
|
public async Task ActualizarAsync(FuenteDato fuenteDato)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
const string sql = @"
|
const string sql = @"UPDATE FuentesDatos
|
||||||
UPDATE FuentesDatos
|
SET UltimaEjecucionExitosa = @UltimaEjecucionExitosa, Url = @Url
|
||||||
SET UltimaEjecucionExitosa = @UltimaEjecucionExitosa, Url = @Url
|
|
||||||
WHERE Id = @Id;";
|
WHERE Id = @Id;";
|
||||||
await connection.ExecuteAsync(sql, fuenteDato);
|
await connection.ExecuteAsync(sql, fuenteDato);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
// Esta interfaz no es estrictamente necesaria ahora, pero es útil para futuras abstracciones.
|
/// <summary>
|
||||||
|
/// Interfaz base marcadora para todos los repositorios.
|
||||||
|
/// No define miembros, pero sirve para la abstracción y la inyección de dependencias.
|
||||||
|
/// </summary>
|
||||||
public interface IBaseRepository
|
public interface IBaseRepository
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,31 @@ using Mercados.Core.Entities;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Define el contrato para el repositorio que gestiona las cotizaciones de la bolsa.
|
||||||
|
/// </summary>
|
||||||
public interface ICotizacionBolsaRepository : IBaseRepository
|
public interface ICotizacionBolsaRepository : IBaseRepository
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Guarda una colección de cotizaciones de bolsa en la base de datos de forma masiva.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cotizaciones">La colección de entidades CotizacionBolsa a guardar.</param>
|
||||||
Task GuardarMuchosAsync(IEnumerable<CotizacionBolsa> cotizaciones);
|
Task GuardarMuchosAsync(IEnumerable<CotizacionBolsa> cotizaciones);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene la última cotización registrada para cada ticker de un mercado específico.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mercado">El código del mercado a consultar (ej. "US", "Local").</param>
|
||||||
|
/// <returns>Una colección con la última cotización de cada activo de ese mercado.</returns>
|
||||||
Task<IEnumerable<CotizacionBolsa>> ObtenerUltimasPorMercadoAsync(string mercado);
|
Task<IEnumerable<CotizacionBolsa>> ObtenerUltimasPorMercadoAsync(string mercado);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene el historial de cotizaciones para un ticker específico durante un período determinado.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ticker">El símbolo del activo (ej. "AAPL", "^MERV").</param>
|
||||||
|
/// <param name="mercado">El mercado al que pertenece el ticker.</param>
|
||||||
|
/// <param name="dias">El número de días hacia atrás desde hoy para obtener el historial.</param>
|
||||||
|
/// <returns>Una colección de cotizaciones ordenadas por fecha de forma ascendente.</returns>
|
||||||
Task<IEnumerable<CotizacionBolsa>> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias);
|
Task<IEnumerable<CotizacionBolsa>> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,30 @@ using Mercados.Core.Entities;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Define el contrato para el repositorio que gestiona las cotizaciones del mercado de ganado.
|
||||||
|
/// </summary>
|
||||||
public interface ICotizacionGanadoRepository : IBaseRepository
|
public interface ICotizacionGanadoRepository : IBaseRepository
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Guarda una colección de cotizaciones de ganado en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cotizaciones">La colección de entidades CotizacionGanado a guardar.</param>
|
||||||
Task GuardarMuchosAsync(IEnumerable<CotizacionGanado> cotizaciones);
|
Task GuardarMuchosAsync(IEnumerable<CotizacionGanado> cotizaciones);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene el último parte completo de cotizaciones del mercado de ganado.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una colección de todas las cotizaciones de la última tanda registrada.</returns>
|
||||||
Task<IEnumerable<CotizacionGanado>> ObtenerUltimaTandaAsync();
|
Task<IEnumerable<CotizacionGanado>> ObtenerUltimaTandaAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene el historial de cotizaciones para una categoría y especificación de ganado.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="categoria">La categoría principal del ganado (ej. "NOVILLOS").</param>
|
||||||
|
/// <param name="especificaciones">La especificación detallada del ganado.</param>
|
||||||
|
/// <param name="dias">El número de días de historial a recuperar.</param>
|
||||||
|
/// <returns>Una colección de cotizaciones históricas para esa categoría.</returns>
|
||||||
Task<IEnumerable<CotizacionGanado>> ObtenerHistorialAsync(string categoria, string especificaciones, int dias);
|
Task<IEnumerable<CotizacionGanado>> ObtenerHistorialAsync(string categoria, string especificaciones, int dias);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,29 @@ using Mercados.Core.Entities;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Define el contrato para el repositorio que gestiona las cotizaciones del mercado de granos.
|
||||||
|
/// </summary>
|
||||||
public interface ICotizacionGranoRepository : IBaseRepository
|
public interface ICotizacionGranoRepository : IBaseRepository
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Guarda una colección de cotizaciones de granos en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cotizaciones">La colección de entidades CotizacionGrano a guardar.</param>
|
||||||
Task GuardarMuchosAsync(IEnumerable<CotizacionGrano> cotizaciones);
|
Task GuardarMuchosAsync(IEnumerable<CotizacionGrano> cotizaciones);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene las últimas cotizaciones disponibles para los granos.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una colección de las últimas cotizaciones de granos registradas.</returns>
|
||||||
Task<IEnumerable<CotizacionGrano>> ObtenerUltimasAsync();
|
Task<IEnumerable<CotizacionGrano>> ObtenerUltimasAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene el historial de cotizaciones para un grano específico.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nombre">El nombre del grano (ej. "Soja").</param>
|
||||||
|
/// <param name="dias">El número de días de historial a recuperar.</param>
|
||||||
|
/// <returns>Una colección de cotizaciones históricas para el grano especificado.</returns>
|
||||||
Task<IEnumerable<CotizacionGrano>> ObtenerHistorialAsync(string nombre, int dias);
|
Task<IEnumerable<CotizacionGrano>> ObtenerHistorialAsync(string nombre, int dias);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,28 @@ using Mercados.Core.Entities;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Define el contrato para el repositorio que gestiona las fuentes de datos.
|
||||||
|
/// </summary>
|
||||||
public interface IFuenteDatoRepository : IBaseRepository
|
public interface IFuenteDatoRepository : IBaseRepository
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene una entidad FuenteDato por su nombre único.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nombre">El nombre de la fuente de datos a buscar.</param>
|
||||||
|
/// <returns>La entidad FuenteDato si se encuentra; de lo contrario, null.</returns>
|
||||||
Task<FuenteDato?> ObtenerPorNombreAsync(string nombre);
|
Task<FuenteDato?> ObtenerPorNombreAsync(string nombre);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Actualiza una entidad FuenteDato existente en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fuenteDato">La entidad FuenteDato con los datos actualizados.</param>
|
||||||
Task ActualizarAsync(FuenteDato fuenteDato);
|
Task ActualizarAsync(FuenteDato fuenteDato);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea una nueva entidad FuenteDato en la base de datos.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fuenteDato">La entidad FuenteDato a crear.</param>
|
||||||
Task CrearAsync(FuenteDato fuenteDato);
|
Task CrearAsync(FuenteDato fuenteDato);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,23 @@ using Mercados.Core.Entities;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Define el contrato para el repositorio que gestiona los feriados de los mercados.
|
||||||
|
/// </summary>
|
||||||
public interface IMercadoFeriadoRepository : IBaseRepository
|
public interface IMercadoFeriadoRepository : IBaseRepository
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene todos los feriados para un mercado y año específicos.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codigoMercado">El código del mercado para el cual se buscan los feriados.</param>
|
||||||
|
/// <param name="anio">El año para el cual se desean obtener los feriados.</param>
|
||||||
|
/// <returns>Una colección de entidades MercadoFeriado para el mercado y año especificados.</returns>
|
||||||
Task<IEnumerable<MercadoFeriado>> ObtenerPorMercadoYAnioAsync(string codigoMercado, int anio);
|
Task<IEnumerable<MercadoFeriado>> ObtenerPorMercadoYAnioAsync(string codigoMercado, int anio);
|
||||||
|
/// <summary>
|
||||||
|
/// Reemplaza todos los feriados existentes para un mercado con una nueva lista.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="codigoMercado">El código del mercado cuyos feriados serán reemplazados.</param>
|
||||||
|
/// <param name="nuevosFeriados">La nueva colección de feriados que se guardará.</param>
|
||||||
Task ReemplazarFeriadosPorMercadoAsync(string codigoMercado, IEnumerable<MercadoFeriado> nuevosFeriados);
|
Task ReemplazarFeriadosPorMercadoAsync(string codigoMercado, IEnumerable<MercadoFeriado> nuevosFeriados);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,24 +4,35 @@ using System.Data;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Persistence.Repositories
|
namespace Mercados.Infrastructure.Persistence.Repositories
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="IMercadoFeriadoRepository"/>
|
||||||
public class MercadoFeriadoRepository : IMercadoFeriadoRepository
|
public class MercadoFeriadoRepository : IMercadoFeriadoRepository
|
||||||
{
|
{
|
||||||
private readonly IDbConnectionFactory _connectionFactory;
|
private readonly IDbConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="MercadoFeriadoRepository"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connectionFactory">Fábrica para crear conexiones a la base de datos.</param>
|
||||||
public MercadoFeriadoRepository(IDbConnectionFactory connectionFactory)
|
public MercadoFeriadoRepository(IDbConnectionFactory connectionFactory)
|
||||||
{
|
{
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<MercadoFeriado>> ObtenerPorMercadoYAnioAsync(string codigoMercado, int anio)
|
public async Task<IEnumerable<MercadoFeriado>> ObtenerPorMercadoYAnioAsync(string codigoMercado, int anio)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
const string sql = @"
|
const string sql = @"SELECT *
|
||||||
SELECT * FROM MercadosFeriados
|
FROM MercadosFeriados
|
||||||
WHERE CodigoMercado = @CodigoMercado AND YEAR(Fecha) = @Anio;";
|
WHERE CodigoMercado = @CodigoMercado AND YEAR(Fecha) = @Anio;";
|
||||||
return await connection.QueryAsync<MercadoFeriado>(sql, new { CodigoMercado = codigoMercado, Anio = anio });
|
return await connection.QueryAsync<MercadoFeriado>(sql, new
|
||||||
|
{
|
||||||
|
CodigoMercado = codigoMercado,
|
||||||
|
Anio = anio
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task ReemplazarFeriadosPorMercadoAsync(string codigoMercado, IEnumerable<MercadoFeriado> nuevosFeriados)
|
public async Task ReemplazarFeriadosPorMercadoAsync(string codigoMercado, IEnumerable<MercadoFeriado> nuevosFeriados)
|
||||||
{
|
{
|
||||||
using IDbConnection connection = _connectionFactory.CreateConnection();
|
using IDbConnection connection = _connectionFactory.CreateConnection();
|
||||||
@@ -30,25 +41,31 @@ namespace Mercados.Infrastructure.Persistence.Repositories
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Borramos todos los feriados del año en curso para ese mercado
|
// Obtenemos el año del primer feriado (asumimos que todos son del mismo año)
|
||||||
var anio = nuevosFeriados.FirstOrDefault()?.Fecha.Year;
|
var anio = nuevosFeriados.FirstOrDefault()?.Fecha.Year;
|
||||||
if (anio.HasValue)
|
if (!anio.HasValue) return; // Si no hay feriados, no hay nada que hacer
|
||||||
|
|
||||||
|
// 1. Borrar los feriados existentes para ese mercado
|
||||||
|
const string deleteSql = "DELETE FROM MercadosFeriados WHERE CodigoMercado = @CodigoMercado;";
|
||||||
|
await connection.ExecuteAsync(deleteSql, new { CodigoMercado = codigoMercado }, transaction);
|
||||||
|
|
||||||
|
// 2. Insertar los nuevos feriados
|
||||||
|
if (nuevosFeriados.Any())
|
||||||
{
|
{
|
||||||
const string deleteSql = "DELETE FROM MercadosFeriados WHERE CodigoMercado = @CodigoMercado AND YEAR(Fecha) = @Anio;";
|
const string insertSql = @"INSERT INTO MercadosFeriados (CodigoMercado, Fecha, Nombre)
|
||||||
await connection.ExecuteAsync(deleteSql, new { CodigoMercado = codigoMercado, Anio = anio.Value }, transaction);
|
VALUES (@CodigoMercado, @Fecha, @Nombre);";
|
||||||
|
await connection.ExecuteAsync(insertSql, nuevosFeriados, transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insertamos los nuevos
|
// Si todo sale bien, confirmar la transacción
|
||||||
const string insertSql = @"
|
|
||||||
INSERT INTO MercadosFeriados (CodigoMercado, Fecha, Nombre)
|
|
||||||
VALUES (@CodigoMercado, @Fecha, @Nombre);";
|
|
||||||
await connection.ExecuteAsync(insertSql, nuevosFeriados, transaction);
|
|
||||||
|
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
// Si hay algún error, deshacer la transacción para no dejar datos inconsistentes
|
||||||
transaction.Rollback();
|
transaction.Rollback();
|
||||||
|
|
||||||
|
// Relanzar la excepción para que el llamador sepa que algo falló
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ using System.Data;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure
|
namespace Mercados.Infrastructure
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Proporciona una fábrica para crear conexiones a la base de datos SQL.
|
||||||
|
/// </summary>
|
||||||
public class SqlConnectionFactory : IDbConnectionFactory
|
public class SqlConnectionFactory : IDbConnectionFactory
|
||||||
{
|
{
|
||||||
private readonly string _connectionString;
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="SqlConnectionFactory"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">La configuración de la aplicación desde donde se obtiene la cadena de conexión.</param>
|
||||||
public SqlConnectionFactory(IConfiguration configuration)
|
public SqlConnectionFactory(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
// Variable de entorno 'DB_CONNECTION_STRING' si está disponible,
|
// Variable de entorno 'DB_CONNECTION_STRING' si está disponible,
|
||||||
@@ -17,9 +24,10 @@ namespace Mercados.Infrastructure
|
|||||||
?? throw new ArgumentNullException(nameof(configuration), "La cadena de conexión 'DefaultConnection' no fue encontrada.");
|
?? throw new ArgumentNullException(nameof(configuration), "La cadena de conexión 'DefaultConnection' no fue encontrada.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDbConnection CreateConnection()
|
/// <inheritdoc />
|
||||||
|
public IDbConnection CreateConnection()
|
||||||
{
|
{
|
||||||
return new SqlConnection(_connectionString);
|
return new SqlConnection(_connectionString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,17 +6,26 @@ using MimeKit;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Services
|
namespace Mercados.Infrastructure.Services
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Servicio que gestiona el envío de notificaciones por correo electrónico.
|
||||||
|
/// </summary>
|
||||||
public class EmailNotificationService : INotificationService
|
public class EmailNotificationService : INotificationService
|
||||||
{
|
{
|
||||||
private readonly ILogger<EmailNotificationService> _logger;
|
private readonly ILogger<EmailNotificationService> _logger;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="EmailNotificationService"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger para registrar información y errores.</param>
|
||||||
|
/// <param name="configuration">Configuración de la aplicación para obtener los ajustes SMTP.</param>
|
||||||
public EmailNotificationService(ILogger<EmailNotificationService> logger, IConfiguration configuration)
|
public EmailNotificationService(ILogger<EmailNotificationService> logger, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task SendFailureAlertAsync(string subject, string message, DateTime? eventTimeUtc = null)
|
public async Task SendFailureAlertAsync(string subject, string message, DateTime? eventTimeUtc = null)
|
||||||
{
|
{
|
||||||
// Leemos la configuración de forma segura desde IConfiguration (que a su vez lee el .env)
|
// Leemos la configuración de forma segura desde IConfiguration (que a su vez lee el .env)
|
||||||
|
|||||||
@@ -5,26 +5,41 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Mercados.Infrastructure.Services
|
namespace Mercados.Infrastructure.Services
|
||||||
{
|
{
|
||||||
public class FinnhubHolidayService : IHolidayService
|
/// <summary>
|
||||||
|
/// Servicio para consultar si una fecha es feriado de mercado utilizando la base de datos interna.
|
||||||
|
/// </summary>
|
||||||
|
public class FinnhubHolidayService : IHolidayService
|
||||||
{
|
{
|
||||||
private readonly IMercadoFeriadoRepository _feriadoRepository;
|
private readonly IMercadoFeriadoRepository _feriadoRepository;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
private readonly ILogger<FinnhubHolidayService> _logger;
|
private readonly ILogger<FinnhubHolidayService> _logger;
|
||||||
|
|
||||||
public FinnhubHolidayService(
|
/// <summary>
|
||||||
IMercadoFeriadoRepository feriadoRepository,
|
/// Inicializa una nueva instancia de la clase <see cref="FinnhubHolidayService"/>.
|
||||||
IMemoryCache cache,
|
/// </summary>
|
||||||
ILogger<FinnhubHolidayService> logger)
|
/// <param name="feriadoRepository">Repositorio para acceder a los feriados de mercado.</param>
|
||||||
{
|
/// <param name="cache">Caché en memoria para almacenar los feriados.</param>
|
||||||
_feriadoRepository = feriadoRepository;
|
/// <param name="logger">Logger para registrar información y errores.</param>
|
||||||
_cache = cache;
|
public FinnhubHolidayService(
|
||||||
_logger = logger;
|
IMercadoFeriadoRepository feriadoRepository,
|
||||||
}
|
IMemoryCache cache,
|
||||||
|
ILogger<FinnhubHolidayService> logger)
|
||||||
|
{
|
||||||
|
_feriadoRepository = feriadoRepository;
|
||||||
|
_cache = cache;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> IsMarketHolidayAsync(string marketCode, DateTime date)
|
/// <summary>
|
||||||
{
|
/// Determina si una fecha específica es feriado de mercado para el código de mercado proporcionado.
|
||||||
var dateOnly = DateOnly.FromDateTime(date);
|
/// </summary>
|
||||||
var cacheKey = $"holidays_{marketCode}_{date.Year}";
|
/// <param name="marketCode">Código del mercado a consultar.</param>
|
||||||
|
/// <param name="date">Fecha a verificar.</param>
|
||||||
|
/// <returns>True si la fecha es feriado de mercado; de lo contrario, false.</returns>
|
||||||
|
public async Task<bool> IsMarketHolidayAsync(string marketCode, DateTime date)
|
||||||
|
{
|
||||||
|
var dateOnly = DateOnly.FromDateTime(date);
|
||||||
|
var cacheKey = $"holidays_{marketCode}_{date.Year}";
|
||||||
|
|
||||||
if (!_cache.TryGetValue(cacheKey, out HashSet<DateOnly>? holidays))
|
if (!_cache.TryGetValue(cacheKey, out HashSet<DateOnly>? holidays))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ namespace Mercados.Infrastructure.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="subject">El título de la alerta.</param>
|
/// <param name="subject">El título de la alerta.</param>
|
||||||
/// <param name="message">El mensaje detallado del error.</param>
|
/// <param name="message">El mensaje detallado del error.</param>
|
||||||
|
/// <param name="eventTimeUtc">La fecha y hora UTC del evento (opcional).</param>
|
||||||
Task SendFailureAlertAsync(string subject, string message, DateTime? eventTimeUtc = null);
|
Task SendFailureAlertAsync(string subject, string message, DateTime? eventTimeUtc = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,23 +14,48 @@ namespace Mercados.Worker
|
|||||||
private readonly ILogger<DataFetchingService> _logger;
|
private readonly ILogger<DataFetchingService> _logger;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly TimeZoneInfo _argentinaTimeZone;
|
private readonly TimeZoneInfo _argentinaTimeZone;
|
||||||
|
|
||||||
// Expresiones Cron
|
/// <summary>
|
||||||
|
/// Expresión Cron para la tarea de Mercado Agroganadero.
|
||||||
|
/// </summary>
|
||||||
private readonly CronExpression _agroSchedule;
|
private readonly CronExpression _agroSchedule;
|
||||||
|
/// <summary>
|
||||||
|
/// Expresión Cron para la tarea de la Bolsa de Comercio de Rosario (BCR).
|
||||||
|
/// </summary>
|
||||||
private readonly CronExpression _bcrSchedule;
|
private readonly CronExpression _bcrSchedule;
|
||||||
|
/// <summary>
|
||||||
|
/// Expresión Cron para la tarea de las Bolsas (Finnhub y Yahoo Finance).
|
||||||
|
/// </summary>
|
||||||
private readonly CronExpression _bolsasSchedule;
|
private readonly CronExpression _bolsasSchedule;
|
||||||
|
/// <summary>
|
||||||
|
/// Expresión Cron para la tarea de actualización de feriados.
|
||||||
|
/// </summary>
|
||||||
private readonly CronExpression _holidaysSchedule;
|
private readonly CronExpression _holidaysSchedule;
|
||||||
|
|
||||||
// Próximas ejecuciones
|
/// <summary>Próxima hora de ejecución programada para la tarea de Mercado Agroganadero.</summary>
|
||||||
private DateTime? _nextAgroRun;
|
private DateTime? _nextAgroRun;
|
||||||
|
/// <summary>Próxima hora de ejecución programada para la tarea de BCR.</summary>
|
||||||
private DateTime? _nextBcrRun;
|
private DateTime? _nextBcrRun;
|
||||||
|
/// <summary>Próxima hora de ejecución programada para la tarea de Bolsas.</summary>
|
||||||
private DateTime? _nextBolsasRun;
|
private DateTime? _nextBolsasRun;
|
||||||
|
/// <summary>Próxima hora de ejecución programada para la tarea de Feriados.</summary>
|
||||||
private DateTime? _nextHolidaysRun;
|
private DateTime? _nextHolidaysRun;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Almacena la última vez que se envió una alerta para una tarea específica, para evitar spam.
|
||||||
|
/// </summary>
|
||||||
private readonly Dictionary<string, DateTime> _lastAlertSent = new();
|
private readonly Dictionary<string, DateTime> _lastAlertSent = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Período de tiempo durante el cual no se enviarán alertas repetidas para la misma tarea.
|
||||||
|
/// </summary>
|
||||||
private readonly TimeSpan _alertSilencePeriod = TimeSpan.FromHours(4);
|
private readonly TimeSpan _alertSilencePeriod = TimeSpan.FromHours(4);
|
||||||
|
|
||||||
// Eliminamos IHolidayService del constructor
|
/// <summary>
|
||||||
|
/// Inicializa una nueva instancia de la clase <see cref="DataFetchingService"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger para registrar información y eventos.</param>
|
||||||
|
/// <param name="serviceProvider">Proveedor de servicios para la inyección de dependencias con scope.</param>
|
||||||
|
/// <param name="configuration">Configuración de la aplicación para obtener los schedules de Cron.</param>
|
||||||
public DataFetchingService(
|
public DataFetchingService(
|
||||||
ILogger<DataFetchingService> logger,
|
ILogger<DataFetchingService> logger,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
@@ -60,8 +85,10 @@ namespace Mercados.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Método principal del servicio. Se ejecuta una vez cuando el servicio arranca.
|
/// Método principal del servicio que se ejecuta en segundo plano. Contiene el bucle
|
||||||
|
/// principal que verifica periódicamente si se debe ejecutar alguna tarea programada.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="stoppingToken">Token de cancelación para detener el servicio de forma segura.</param>
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("🚀 Servicio de Fetching iniciado a las: {time}", DateTimeOffset.Now);
|
_logger.LogInformation("🚀 Servicio de Fetching iniciado a las: {time}", DateTimeOffset.Now);
|
||||||
@@ -154,6 +181,9 @@ namespace Mercados.Worker
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ejecuta un fetcher específico por su nombre, gestionando el scope de DI y las notificaciones.
|
/// Ejecuta un fetcher específico por su nombre, gestionando el scope de DI y las notificaciones.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="sourceName">El nombre del <see cref="IDataFetcher"/> a ejecutar.</param>
|
||||||
|
/// <param name="stoppingToken">Token de cancelación para detener la operación si el servicio se está parando.</param>
|
||||||
|
/// <remarks>Este método crea un nuevo scope de DI para resolver los servicios necesarios.</remarks>
|
||||||
private async Task RunFetcherByNameAsync(string sourceName, CancellationToken stoppingToken)
|
private async Task RunFetcherByNameAsync(string sourceName, CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) return;
|
if (stoppingToken.IsCancellationRequested) return;
|
||||||
@@ -193,6 +223,8 @@ namespace Mercados.Worker
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ejecuta todos los fetchers en paralelo al iniciar el servicio.
|
/// Ejecuta todos los fetchers en paralelo al iniciar el servicio.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="stoppingToken">Token de cancelación para detener la operación si el servicio se está parando.</param>
|
||||||
|
/// <remarks>Esta función se usa principalmente para una ejecución de prueba al arrancar.</remarks>
|
||||||
private async Task RunAllFetchersAsync(CancellationToken stoppingToken)
|
private async Task RunAllFetchersAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Ejecutando todos los fetchers al iniciar en paralelo...");
|
_logger.LogInformation("Ejecutando todos los fetchers al iniciar en paralelo...");
|
||||||
@@ -211,6 +243,8 @@ namespace Mercados.Worker
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determina si se debe enviar una alerta o si está en período de silencio.
|
/// Determina si se debe enviar una alerta o si está en período de silencio.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="taskName">El nombre de la tarea que podría generar la alerta.</param>
|
||||||
|
/// <returns>True si se debe enviar la alerta; de lo contrario, false.</returns>
|
||||||
private bool ShouldSendAlert(string taskName)
|
private bool ShouldSendAlert(string taskName)
|
||||||
{
|
{
|
||||||
if (!_lastAlertSent.ContainsKey(taskName))
|
if (!_lastAlertSent.ContainsKey(taskName))
|
||||||
@@ -224,8 +258,13 @@ namespace Mercados.Worker
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
// Creamos una única función para comprobar feriados que obtiene el servicio
|
/// <summary>
|
||||||
// desde un scope.
|
/// Comprueba si una fecha dada es feriado para un mercado específico.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="marketCode">El código del mercado (ej. "US", "BA").</param>
|
||||||
|
/// <param name="date">La fecha a comprobar.</param>
|
||||||
|
/// <returns>True si es feriado, false si no lo es o si ocurre un error.</returns>
|
||||||
|
/// <remarks>Este método resuelve el <see cref="IHolidayService"/> desde un nuevo scope de DI para cada llamada.</remarks>
|
||||||
private async Task<bool> IsMarketHolidayAsync(string marketCode, DateTime date)
|
private async Task<bool> IsMarketHolidayAsync(string marketCode, DateTime date)
|
||||||
{
|
{
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>dotnet-Mercados.Worker-0e9a6e84-c8fb-400b-ba7b-193d9981a046</UserSecretsId>
|
<UserSecretsId>dotnet-Mercados.Worker-0e9a6e84-c8fb-400b-ba7b-193d9981a046</UserSecretsId>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user