feat(core): add internal crypto isin resolution and subtitle mapping
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace FinlyticCore.Converters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ValueConverter for DateTime guaranteeing DateTimeKind.Utc when writing to and reading from PostgreSQL.
|
||||||
|
/// </summary>
|
||||||
|
public class UtcDateTimeConverter : ValueConverter<DateTime, DateTime>
|
||||||
|
{
|
||||||
|
public UtcDateTimeConverter()
|
||||||
|
: base(
|
||||||
|
v => v.Kind == DateTimeKind.Utc ? v : DateTime.SpecifyKind(v, DateTimeKind.Utc),
|
||||||
|
v => DateTime.SpecifyKind(v, DateTimeKind.Utc))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ValueConverter for nullable DateTime? guaranteeing DateTimeKind.Utc when writing to and reading from PostgreSQL.
|
||||||
|
/// </summary>
|
||||||
|
public class NullableUtcDateTimeConverter : ValueConverter<DateTime?, DateTime?>
|
||||||
|
{
|
||||||
|
public NullableUtcDateTimeConverter()
|
||||||
|
: base(
|
||||||
|
v => v.HasValue ? (v.Value.Kind == DateTimeKind.Utc ? v.Value : DateTime.SpecifyKind(v.Value, DateTimeKind.Utc)) : v,
|
||||||
|
v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace FinlyticCore.Dtos.Fundamentals;
|
namespace FinlyticCore.Dtos.Fundamentals;
|
||||||
|
|
||||||
@@ -19,4 +19,7 @@ public record KeyExecutiveDto
|
|||||||
|
|
||||||
[JsonPropertyName("payment")]
|
[JsonPropertyName("payment")]
|
||||||
public string Payment { get; init; } = string.Empty;
|
public string Payment { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("sortOrder")]
|
||||||
|
public int SortOrder { get; init; } = 0;
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.Playwright" Version="1.49.0" />
|
<PackageReference Include="Microsoft.Playwright" Version="1.49.0" />
|
||||||
<PackageReference Include="MQTTnet" Version="5.1.0.1559" />
|
<PackageReference Include="MQTTnet" Version="5.1.0.1559" />
|
||||||
|
<PackageReference Include="Npgsql" Version="10.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ public class CloseTradeRequest
|
|||||||
{
|
{
|
||||||
public decimal UserExitPrice { get; set; }
|
public decimal UserExitPrice { get; set; }
|
||||||
public DateTime? UserExitTimestamp { get; set; }
|
public DateTime? UserExitTimestamp { get; set; }
|
||||||
|
public decimal ExitFee { get; set; } = 1.0m;
|
||||||
public string CloseReason { get; set; } = "ManualClosure"; // "TakeProfitHit", "StopLossHit", "ManualClosure", "TimeExpired"
|
public string CloseReason { get; set; } = "ManualClosure"; // "TakeProfitHit", "StopLossHit", "ManualClosure", "TimeExpired"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ public class TradeProposalDto
|
|||||||
[JsonPropertyName("instrumentType")]
|
[JsonPropertyName("instrumentType")]
|
||||||
public string InstrumentType { get; set; } = "Stock"; // "Stock", "Option", "CFD", "Crypto"
|
public string InstrumentType { get; set; } = "Stock"; // "Stock", "Option", "CFD", "Crypto"
|
||||||
|
|
||||||
|
[JsonPropertyName("assetType")]
|
||||||
|
public string AssetType { get; set; } = "stock"; // "stock", "etf", "crypto", "bond"
|
||||||
|
|
||||||
|
[JsonPropertyName("hasCfd")]
|
||||||
|
public bool HasCfd { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("derivativeProductCategories")]
|
||||||
|
public List<string> DerivativeProductCategories { get; set; } = new();
|
||||||
|
|
||||||
[JsonPropertyName("derivativeIsin")]
|
[JsonPropertyName("derivativeIsin")]
|
||||||
public string? DerivativeIsin { get; set; }
|
public string? DerivativeIsin { get; set; }
|
||||||
|
|
||||||
@@ -141,6 +150,21 @@ public class TradeProposalDto
|
|||||||
[JsonPropertyName("pnlPercent")]
|
[JsonPropertyName("pnlPercent")]
|
||||||
public decimal? PnlPercent { get; set; }
|
public decimal? PnlPercent { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("closeReason")]
|
||||||
|
public string? CloseReason { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("userExitTimestamp")]
|
||||||
|
public DateTime? UserExitTimestamp { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("hasPendingExitAlert")]
|
||||||
|
public bool HasPendingExitAlert { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("pendingExitReason")]
|
||||||
|
public string? PendingExitReason { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("hourlyUpdates")]
|
||||||
|
public List<TradeHourlyUpdateDto>? HourlyUpdates { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("createdAt")]
|
[JsonPropertyName("createdAt")]
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
namespace FinlyticCore.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the crypto subtitle/ticker (e.g. "BTC", "ETH", "SOL") for Trade Republic internal ISINs starting with 'X'.
|
||||||
|
/// </summary>
|
||||||
|
public static class CryptoSubtitleResolver
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, (string Subtitle, string? Name)> _cache = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if an ISIN is a Trade Republic internal crypto ISIN (starts with 'X') and resolves its Subtitle from DB or heuristic.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<(string? Subtitle, string? Name)> ResolveCryptoInfoAsync(
|
||||||
|
string isin,
|
||||||
|
string? defaultConnectionString = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(isin)) return (null, null);
|
||||||
|
|
||||||
|
var cleanIsin = isin.Trim().ToUpperInvariant();
|
||||||
|
if (!cleanIsin.StartsWith("X", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_cache.TryGetValue(cleanIsin, out var cached))
|
||||||
|
{
|
||||||
|
return (cached.Subtitle, cached.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Try querying PostgreSQL database (finlytic_assets)
|
||||||
|
if (!string.IsNullOrWhiteSpace(defaultConnectionString))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assetsConnStr = Regex.Replace(defaultConnectionString, @"Database=[^;]+", "Database=finlytic_assets", RegexOptions.IgnoreCase);
|
||||||
|
await using var conn = new NpgsqlConnection(assetsConnStr);
|
||||||
|
await conn.OpenAsync(cancellationToken);
|
||||||
|
|
||||||
|
await using var cmd = new NpgsqlCommand(
|
||||||
|
"SELECT \"Subtitle\", \"SearchSubtitle\", \"Name\" FROM \"TradeRepublicAssets\" " +
|
||||||
|
"WHERE \"Isin\" = @isin AND (\"AssetType\" = 'Crypto' OR \"InstrumentCategory\" = 'crypto' OR \"Subtitle\" IS NOT NULL) " +
|
||||||
|
"LIMIT 1",
|
||||||
|
conn);
|
||||||
|
cmd.Parameters.AddWithValue("isin", cleanIsin);
|
||||||
|
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||||
|
if (await reader.ReadAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
string? sub = reader.IsDBNull(0) ? null : reader.GetString(0);
|
||||||
|
if (string.IsNullOrWhiteSpace(sub) && !reader.IsDBNull(1))
|
||||||
|
{
|
||||||
|
sub = reader.GetString(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
string? name = reader.IsDBNull(2) ? null : reader.GetString(2);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(sub))
|
||||||
|
{
|
||||||
|
var cleanSub = sub.Trim().ToUpperInvariant();
|
||||||
|
_cache[cleanIsin] = (cleanSub, name);
|
||||||
|
return (cleanSub, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fall through to heuristic if DB unreachable or different server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Heuristic fallback for Trade Republic internal ISIN patterns (e.g. XF000BTC0017 -> BTC)
|
||||||
|
var match = Regex.Match(cleanIsin, @"^X[A-Z0-9]*?000([A-Z0-9]{3,6})\d*$");
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
var extracted = match.Groups[1].Value;
|
||||||
|
_cache[cleanIsin] = (extracted, null);
|
||||||
|
return (extracted, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convenience method returning just the crypto subtitle (e.g. "BTC").
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<string?> ResolveCryptoSubtitleAsync(
|
||||||
|
string isin,
|
||||||
|
string? defaultConnectionString = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var (sub, _) = await ResolveCryptoInfoAsync(isin, defaultConnectionString, cancellationToken);
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,15 +45,18 @@ public class YahooFinanceScraper : IYahooFinanceScraper
|
|||||||
{
|
{
|
||||||
private readonly YahooFinanceClient _yahooApiClient;
|
private readonly YahooFinanceClient _yahooApiClient;
|
||||||
private readonly IYahooFinanceHtmlClient _htmlScraperClient;
|
private readonly IYahooFinanceHtmlClient _htmlScraperClient;
|
||||||
|
private readonly Microsoft.Extensions.Configuration.IConfiguration _configuration;
|
||||||
private readonly IFinlyticLogger<YahooFinanceScraper, FundamentalsDbContext> _finlyticLogger;
|
private readonly IFinlyticLogger<YahooFinanceScraper, FundamentalsDbContext> _finlyticLogger;
|
||||||
|
|
||||||
public YahooFinanceScraper(
|
public YahooFinanceScraper(
|
||||||
YahooFinanceClient yahooApiClient,
|
YahooFinanceClient yahooApiClient,
|
||||||
IYahooFinanceHtmlClient htmlScraperClient,
|
IYahooFinanceHtmlClient htmlScraperClient,
|
||||||
|
Microsoft.Extensions.Configuration.IConfiguration configuration,
|
||||||
IFinlyticLogger<YahooFinanceScraper, FundamentalsDbContext> finlyticLogger)
|
IFinlyticLogger<YahooFinanceScraper, FundamentalsDbContext> finlyticLogger)
|
||||||
{
|
{
|
||||||
_yahooApiClient = yahooApiClient;
|
_yahooApiClient = yahooApiClient;
|
||||||
_htmlScraperClient = htmlScraperClient;
|
_htmlScraperClient = htmlScraperClient;
|
||||||
|
_configuration = configuration;
|
||||||
_finlyticLogger = finlyticLogger;
|
_finlyticLogger = finlyticLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,21 +75,72 @@ public class YahooFinanceScraper : IYahooFinanceScraper
|
|||||||
var cleanIsin = isin.Trim().ToUpperInvariant();
|
var cleanIsin = isin.Trim().ToUpperInvariant();
|
||||||
var symbols = new List<(string symbol, string exchange, int priority)>();
|
var symbols = new List<(string symbol, string exchange, int priority)>();
|
||||||
|
|
||||||
|
// Crypto / Trade Republic interne ISINs (beginnend mit 'X', z. B. XF000BTC0017)
|
||||||
|
if (cleanIsin.StartsWith("X", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var (cryptoSubtitle, cryptoName) = await FinlyticCore.Utils.CryptoSubtitleResolver.ResolveCryptoInfoAsync(
|
||||||
|
cleanIsin, _configuration.GetConnectionString("DefaultConnection"), cancellationToken);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(cryptoSubtitle))
|
||||||
|
{
|
||||||
|
var cryptoEur = $"{cryptoSubtitle}-EUR";
|
||||||
|
var cryptoUsd = $"{cryptoSubtitle}-USD";
|
||||||
|
|
||||||
|
symbols.Add((cryptoEur, "Crypto", 0));
|
||||||
|
symbols.Add((cryptoUsd, "Crypto", 1));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var searchRes = await _yahooApiClient.SearchAsync(cryptoSubtitle, quotesCount: 10, cancellationToken: cancellationToken);
|
||||||
|
if (searchRes?.Quotes != null)
|
||||||
|
{
|
||||||
|
foreach (var q in searchRes.Quotes.Where(q => !string.IsNullOrEmpty(q.Symbol)))
|
||||||
|
{
|
||||||
|
if (!symbols.Any(s => s.symbol.Equals(q.Symbol, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
symbols.Add((q.Symbol, q.Exchange ?? "Crypto", 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
await _finlyticLogger.LogInfoAsync(SettingKeys.FundamentalsChannel,
|
||||||
|
"[YahooFinanceScraper] Resolved Crypto ISIN {Isin} to {Symbol} using Subtitle {Sub}",
|
||||||
|
cleanIsin, cryptoEur, cryptoSubtitle);
|
||||||
|
|
||||||
|
return symbols
|
||||||
|
.OrderBy(s => s.priority)
|
||||||
|
.Select(s => new TickerInfoDto { Ticker = s.symbol, Exchange = s.exchange })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Suche via ISIN
|
// 1. Suche via ISIN - der allererste Ticker von Yahoo Finance ist der absolute Primary Ticker
|
||||||
var primary = await _yahooApiClient.SearchAsync(cleanIsin, quotesCount: 20, cancellationToken: cancellationToken);
|
var primary = await _yahooApiClient.SearchAsync(cleanIsin, quotesCount: 20, cancellationToken: cancellationToken);
|
||||||
var quotes = primary?.Quotes ?? new List<YahooSearchQuoteDto>();
|
var quotes = primary?.Quotes ?? new List<YahooSearchQuoteDto>();
|
||||||
|
var validQuotes = quotes.Where(q => !string.IsNullOrEmpty(q.Symbol)).ToList();
|
||||||
|
|
||||||
foreach (var q in quotes.Where(q => !string.IsNullOrEmpty(q.Symbol)))
|
if (validQuotes.Count > 0)
|
||||||
{
|
{
|
||||||
symbols.Add((q.Symbol, q.Exchange ?? string.Empty, GetExchangePriority(q.Symbol, cleanIsin)));
|
var first = validQuotes[0];
|
||||||
|
symbols.Add((first.Symbol, first.Exchange ?? string.Empty, 0));
|
||||||
|
|
||||||
|
foreach (var q in validQuotes.Skip(1))
|
||||||
|
{
|
||||||
|
if (!symbols.Any(s => s.symbol.Equals(q.Symbol, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
symbols.Add((q.Symbol, q.Exchange ?? string.Empty, Math.Max(1, GetExchangePriority(q.Symbol, cleanIsin))));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Falls Ticker gefunden, aber mit Unternehmensname noch mehr Exchangeticker auffindbar sind
|
// 2. Falls Ticker gefunden, aber mit Unternehmensname noch mehr Exchangeticker auffindbar sind
|
||||||
if (quotes.Count > 0)
|
if (validQuotes.Count > 0)
|
||||||
{
|
{
|
||||||
var companyName = quotes[0].LongName ?? quotes[0].ShortName;
|
var companyName = validQuotes[0].LongName ?? validQuotes[0].ShortName;
|
||||||
if (!string.IsNullOrWhiteSpace(companyName))
|
if (!string.IsNullOrWhiteSpace(companyName))
|
||||||
{
|
{
|
||||||
var secondary = await _yahooApiClient.SearchAsync(companyName, quotesCount: 20, cancellationToken: cancellationToken);
|
var secondary = await _yahooApiClient.SearchAsync(companyName, quotesCount: 20, cancellationToken: cancellationToken);
|
||||||
@@ -95,7 +149,7 @@ public class YahooFinanceScraper : IYahooFinanceScraper
|
|||||||
if (!string.IsNullOrEmpty(q.Symbol) &&
|
if (!string.IsNullOrEmpty(q.Symbol) &&
|
||||||
!symbols.Any(s => s.symbol.Equals(q.Symbol, StringComparison.OrdinalIgnoreCase)))
|
!symbols.Any(s => s.symbol.Equals(q.Symbol, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
symbols.Add((q.Symbol, q.Exchange ?? string.Empty, GetExchangePriority(q.Symbol, cleanIsin)));
|
symbols.Add((q.Symbol, q.Exchange ?? string.Empty, Math.Max(1, GetExchangePriority(q.Symbol, cleanIsin))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ public class TechnicalAnalysisDbService : ITechnicalAnalysisDbService
|
|||||||
var macroTask = FetchMacroDataAsync(cancellationToken);
|
var macroTask = FetchMacroDataAsync(cancellationToken);
|
||||||
|
|
||||||
string? ticker = requestedTicker;
|
string? ticker = requestedTicker;
|
||||||
if (string.IsNullOrWhiteSpace(ticker))
|
if (string.IsNullOrWhiteSpace(ticker) || string.Equals(ticker.Trim(), cleanIsin, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
ticker = await _yahooScraper.ResolveTickerFromIsinAsync(cleanIsin, cancellationToken);
|
ticker = await _yahooScraper.ResolveTickerFromIsinAsync(cleanIsin, cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -326,7 +326,9 @@ public class TechnicalAnalysisDbService : ITechnicalAnalysisDbService
|
|||||||
|
|
||||||
if (cached != null && DateTime.UtcNow - cached.CalculatedAt < DbCacheTtl)
|
if (cached != null && DateTime.UtcNow - cached.CalculatedAt < DbCacheTtl)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(requestedTicker) && !string.Equals(cached.Ticker, requestedTicker, StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrWhiteSpace(requestedTicker) &&
|
||||||
|
!string.Equals(requestedTicker.Trim(), cleanIsin, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!string.Equals(cached.Ticker, requestedTicker, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return null; // Ticker mismatch, force refresh required
|
return null; // Ticker mismatch, force refresh required
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,16 +40,21 @@ public interface IYahooMarketDataScraper
|
|||||||
public class YahooMarketDataScraper : IYahooMarketDataScraper
|
public class YahooMarketDataScraper : IYahooMarketDataScraper
|
||||||
{
|
{
|
||||||
private readonly YahooFinanceClient _yahooClient;
|
private readonly YahooFinanceClient _yahooClient;
|
||||||
|
private readonly Microsoft.Extensions.Configuration.IConfiguration _configuration;
|
||||||
private readonly ILogger<YahooMarketDataScraper> _logger;
|
private readonly ILogger<YahooMarketDataScraper> _logger;
|
||||||
|
|
||||||
public YahooMarketDataScraper(YahooFinanceClient yahooClient, ILogger<YahooMarketDataScraper> logger)
|
public YahooMarketDataScraper(
|
||||||
|
YahooFinanceClient yahooClient,
|
||||||
|
Microsoft.Extensions.Configuration.IConfiguration configuration,
|
||||||
|
ILogger<YahooMarketDataScraper> logger)
|
||||||
{
|
{
|
||||||
_yahooClient = yahooClient;
|
_yahooClient = yahooClient;
|
||||||
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves ticker from ISIN using Yahoo Search API.
|
/// Resolves ticker from ISIN using Yahoo Search API or Crypto Subtitle resolution for internal ISINs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<string?> ResolveTickerFromIsinAsync(string isin, CancellationToken cancellationToken = default)
|
public async Task<string?> ResolveTickerFromIsinAsync(string isin, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -61,6 +66,34 @@ public class YahooMarketDataScraper : IYahooMarketDataScraper
|
|||||||
return cleanIsin;
|
return cleanIsin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Crypto / Trade Republic interne ISINs (beginnend mit 'X', z. B. XF000BTC0017)
|
||||||
|
if (cleanIsin.StartsWith("X", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var (cryptoSubtitle, cryptoName) = await FinlyticCore.Utils.CryptoSubtitleResolver.ResolveCryptoInfoAsync(
|
||||||
|
cleanIsin, _configuration.GetConnectionString("DefaultConnection"), cancellationToken);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(cryptoSubtitle))
|
||||||
|
{
|
||||||
|
var candidates = new[] { $"{cryptoSubtitle}-EUR", $"{cryptoSubtitle}-USD", cryptoSubtitle };
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await FetchHistoricalCandlesWithCurrencyAsync(candidate, "5d", "1d", cancellationToken);
|
||||||
|
if (res.Candles.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] Resolved Crypto ISIN {Isin} to {Symbol} using Subtitle {Sub}",
|
||||||
|
"TechnicalAnalysisChannel", cleanIsin, candidate, cryptoSubtitle);
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{cryptoSubtitle}-EUR";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var searchResult = await _yahooClient.SearchAsync(cleanIsin, quotesCount: 10, newsCount: 0, cancellationToken);
|
var searchResult = await _yahooClient.SearchAsync(cleanIsin, quotesCount: 10, newsCount: 0, cancellationToken);
|
||||||
|
|||||||
Reference in New Issue
Block a user