feat(Fundamentals): add fundamentals service
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
using FinlyticFundamentals.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Database;
|
||||||
|
|
||||||
|
public class FundamentalsDbContext : DbContext
|
||||||
|
{
|
||||||
|
public FundamentalsDbContext(DbContextOptions<FundamentalsDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<AssetFundamentalsEntity> AssetFundamentals => Set<AssetFundamentalsEntity>();
|
||||||
|
public DbSet<CompanyExecutiveEntity> CompanyExecutives => Set<CompanyExecutiveEntity>();
|
||||||
|
public DbSet<FinancialStatementEntity> FinancialStatements => Set<FinancialStatementEntity>();
|
||||||
|
public DbSet<ForwardEstimateEntity> ForwardEstimates => Set<ForwardEstimateEntity>();
|
||||||
|
public DbSet<TickerFundamentalsEntity> TickerFundamentals => Set<TickerFundamentalsEntity>();
|
||||||
|
public DbSet<FundamentalsSettingsEntity> Settings => Set<FundamentalsSettingsEntity>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
// AssetFundamentals Configurations
|
||||||
|
modelBuilder.Entity<AssetFundamentalsEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Isin);
|
||||||
|
entity.HasIndex(e => e.PrimaryTicker).IsUnique();
|
||||||
|
|
||||||
|
// Setup One-to-Many Relationships with cascades
|
||||||
|
entity.HasMany(e => e.Executives)
|
||||||
|
.WithOne(e => e.AssetFundamentals)
|
||||||
|
.HasForeignKey(e => e.Isin)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
entity.HasMany(e => e.FinancialStatements)
|
||||||
|
.WithOne(e => e.AssetFundamentals)
|
||||||
|
.HasForeignKey(e => e.Isin)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
entity.HasMany(e => e.Estimates)
|
||||||
|
.WithOne(e => e.AssetFundamentals)
|
||||||
|
.HasForeignKey(e => e.Isin)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
entity.HasMany(e => e.TickerFundamentals)
|
||||||
|
.WithOne(e => e.AssetFundamentals)
|
||||||
|
.HasForeignKey(e => e.Isin)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
// CompanyExecutive Configurations
|
||||||
|
modelBuilder.Entity<CompanyExecutiveEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => e.Isin);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FinancialStatement Configurations
|
||||||
|
modelBuilder.Entity<FinancialStatementEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => e.Isin);
|
||||||
|
// Compound Index to prevent duplicate statement entries
|
||||||
|
entity.HasIndex(e => new { e.Isin, e.PeriodType, e.EndDate }).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ForwardEstimate Configurations
|
||||||
|
modelBuilder.Entity<ForwardEstimateEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => e.Isin);
|
||||||
|
entity.HasIndex(e => new { e.Isin, e.Period }).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
// TickerFundamentals Configurations
|
||||||
|
modelBuilder.Entity<TickerFundamentalsEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Ticker);
|
||||||
|
entity.HasIndex(e => e.Isin);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base
|
||||||
|
USER $APP_UID
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["FinlyticFundamentals/FinlyticFundamentals.csproj", "FinlyticFundamentals/"]
|
||||||
|
COPY ["FinlyticCore/FinlyticCore.csproj", "FinlyticCore/"]
|
||||||
|
RUN dotnet restore "FinlyticFundamentals/FinlyticFundamentals.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/FinlyticFundamentals"
|
||||||
|
RUN dotnet build "./FinlyticFundamentals.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
RUN dotnet publish "./FinlyticFundamentals.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "FinlyticFundamentals.dll"]
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main database table representing global company profile, ownership structure, analyst predictions, and corporate events.
|
||||||
|
/// </summary>
|
||||||
|
public class AssetFundamentalsEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
[Required]
|
||||||
|
public string Isin { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string PrimaryTicker { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// --- Static Company Profile Columns ---
|
||||||
|
[Required]
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
public string? BusinessSummary { get; set; }
|
||||||
|
public string? Sector { get; set; }
|
||||||
|
public string? Industry { get; set; }
|
||||||
|
public string? Country { get; set; }
|
||||||
|
public int? Employees { get; set; }
|
||||||
|
|
||||||
|
// --- Ownership & Sentiment Data (Company-wide) ---
|
||||||
|
public decimal? PercentHeldByInstitutions { get; set; }
|
||||||
|
public decimal? PercentHeldByInsiders { get; set; }
|
||||||
|
public decimal? ShortRatio { get; set; }
|
||||||
|
public decimal? ShortPercentOfFloat { get; set; }
|
||||||
|
|
||||||
|
// --- Analyst Forecasts & Targets ---
|
||||||
|
public string? ConsensusRating { get; set; }
|
||||||
|
public decimal? PriceTargetLow { get; set; }
|
||||||
|
public decimal? PriceTargetHigh { get; set; }
|
||||||
|
public decimal? PriceTargetMedian { get; set; }
|
||||||
|
public decimal? PriceTargetMean { get; set; }
|
||||||
|
|
||||||
|
// --- Corporate Calendar / Catalysts ---
|
||||||
|
public DateTime? ExDividendDate { get; set; }
|
||||||
|
public DateTime? NextEarningsDate { get; set; }
|
||||||
|
|
||||||
|
// --- Cache Control Timestamps ---
|
||||||
|
public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime LastStaticUpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// --- Relational Collections ---
|
||||||
|
public List<CompanyExecutiveEntity> Executives { get; set; } = [];
|
||||||
|
public List<FinancialStatementEntity> FinancialStatements { get; set; } = [];
|
||||||
|
public List<ForwardEstimateEntity> Estimates { get; set; } = [];
|
||||||
|
public List<TickerFundamentalsEntity> TickerFundamentals { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Relational executive board table linked to the main fundamentals entity by ISIN.
|
||||||
|
/// </summary>
|
||||||
|
public class CompanyExecutiveEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Isin { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public AssetFundamentalsEntity? AssetFundamentals { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public int? Age { get; set; }
|
||||||
|
public decimal? Compensation { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Relational statement table linking historical Income Statements, Balance Sheets, and Cash Flow metrics.
|
||||||
|
/// </summary>
|
||||||
|
public class FinancialStatementEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Isin { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public AssetFundamentalsEntity? AssetFundamentals { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string PeriodType { get; set; } = string.Empty; // "Annual" or "Quarterly"
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime EndDate { get; set; }
|
||||||
|
|
||||||
|
// --- Income Statement Fields ---
|
||||||
|
public decimal? TotalRevenue { get; set; }
|
||||||
|
public decimal? CostOfRevenue { get; set; }
|
||||||
|
public decimal? GrossProfit { get; set; }
|
||||||
|
public decimal? OperatingExpenses { get; set; }
|
||||||
|
public decimal? OperatingIncome { get; set; }
|
||||||
|
public decimal? Ebitda { get; set; }
|
||||||
|
public decimal? NetIncome { get; set; }
|
||||||
|
public decimal? EpsBasic { get; set; }
|
||||||
|
public decimal? EpsDiluted { get; set; }
|
||||||
|
|
||||||
|
// --- Balance Sheet Fields ---
|
||||||
|
public decimal? CashAndCashEquivalents { get; set; }
|
||||||
|
public decimal? AccountsReceivable { get; set; }
|
||||||
|
public decimal? Inventory { get; set; }
|
||||||
|
public decimal? TotalCurrentAssets { get; set; }
|
||||||
|
public decimal? TotalNonCurrentAssets { get; set; }
|
||||||
|
public decimal? CurrentLiabilities { get; set; }
|
||||||
|
public decimal? LongTermDebt { get; set; }
|
||||||
|
public decimal? TotalLiabilities { get; set; }
|
||||||
|
public decimal? TotalStockholdersEquity { get; set; }
|
||||||
|
|
||||||
|
// --- Cash Flow Fields ---
|
||||||
|
public decimal? OperatingCashFlow { get; set; }
|
||||||
|
public decimal? InvestingCashFlow { get; set; }
|
||||||
|
public decimal? CapitalExpenditures { get; set; }
|
||||||
|
public decimal? FinancingCashFlow { get; set; }
|
||||||
|
public decimal? FreeCashFlow { get; set; } // OperatingCashFlow - CapEx
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Relational table for analyst consensus revenue and EPS forecasts.
|
||||||
|
/// </summary>
|
||||||
|
public class ForwardEstimateEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Isin { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public AssetFundamentalsEntity? AssetFundamentals { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Period { get; set; } = string.Empty; // "CurrentQuarter", "NextQuarter", "CurrentYear", "NextYear"
|
||||||
|
public decimal? ExpectedRevenue { get; set; }
|
||||||
|
public decimal? ExpectedEps { get; set; }
|
||||||
|
public decimal? ExpectedGrowthRate { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Entities;
|
||||||
|
|
||||||
|
public class FundamentalsSettingsEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public int CacheTtlHours { get; set; } = 24;
|
||||||
|
public bool EnableYahooFallback { get; set; } = true;
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Database table representing dynamic trading data, exchange-dependent valuation ratios, liquidity, and leverage stats for each traded ticker symbol.
|
||||||
|
/// </summary>
|
||||||
|
public class TickerFundamentalsEntity
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
[Required]
|
||||||
|
public string Ticker { get; set; } = string.Empty; // Primary key, e.g., "POR.DE"
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Isin { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[ForeignKey("Isin")]
|
||||||
|
public AssetFundamentalsEntity? AssetFundamentals { get; set; }
|
||||||
|
|
||||||
|
public string? Exchange { get; set; }
|
||||||
|
public string? TradingCurrency { get; set; }
|
||||||
|
|
||||||
|
// Real-time & Price Performance
|
||||||
|
public decimal CurrentPrice { get; set; }
|
||||||
|
public decimal DayChangeAbsolute { get; set; }
|
||||||
|
public decimal DayChangePercent { get; set; }
|
||||||
|
public decimal FiftyTwoWeekHigh { get; set; }
|
||||||
|
public decimal FiftyTwoWeekLow { get; set; }
|
||||||
|
|
||||||
|
// Size & Enterprise Valuation
|
||||||
|
public decimal MarketCapitalization { get; set; }
|
||||||
|
public decimal EnterpriseValue { get; set; }
|
||||||
|
|
||||||
|
// Valuation Ratios
|
||||||
|
public decimal? PeRatioTrailing { get; set; }
|
||||||
|
public decimal? PeRatioForward { get; set; }
|
||||||
|
public decimal? PegRatio { get; set; }
|
||||||
|
public decimal? PbRatio { get; set; }
|
||||||
|
public decimal? PsRatio { get; set; }
|
||||||
|
public decimal? EvToEbitda { get; set; }
|
||||||
|
public decimal? EvToRevenue { get; set; }
|
||||||
|
|
||||||
|
// Profitability & Return Ratios
|
||||||
|
public decimal? GrossMargin { get; set; }
|
||||||
|
public decimal? OperatingMargin { get; set; }
|
||||||
|
public decimal? NetProfitMargin { get; set; }
|
||||||
|
public decimal? ReturnOnEquity { get; set; }
|
||||||
|
public decimal? ReturnOnAssets { get; set; }
|
||||||
|
public decimal? ReturnOnInvestedCapital { get; set; }
|
||||||
|
|
||||||
|
// Financial Health, Solvency & Liquidity
|
||||||
|
public decimal? DebtToEquity { get; set; }
|
||||||
|
public decimal? CurrentRatio { get; set; }
|
||||||
|
public decimal? QuickRatio { get; set; }
|
||||||
|
public decimal? InterestCoverage { get; set; }
|
||||||
|
|
||||||
|
// Dividend Performance Metrics
|
||||||
|
public decimal? DividendYield { get; set; }
|
||||||
|
public decimal? PayoutRatio { get; set; }
|
||||||
|
public DateTime? ExDividendDate { get; set; }
|
||||||
|
|
||||||
|
public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FinlyticCore\FinlyticCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FinlyticFundamentals.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(FundamentalsDbContext))]
|
||||||
|
[Migration("20260801073430_Init")]
|
||||||
|
partial class Init
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetFundamentalsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("BusinessSummary")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ConsensusRating")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Country")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int?>("Employees")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExDividendDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Industry")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastStaticUpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("NextEarningsDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PercentHeldByInsiders")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PercentHeldByInstitutions")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetHigh")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetLow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetMean")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetMedian")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("PrimaryTicker")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Sector")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ShortPercentOfFloat")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ShortRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.HasKey("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("PrimaryTicker")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AssetFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.CompanyExecutiveEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int?>("Age")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Compensation")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.ToTable("CompanyExecutives");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.FinancialStatementEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("AccountsReceivable")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CapitalExpenditures")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CashAndCashEquivalents")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CostOfRevenue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CurrentLiabilities")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Ebitda")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<DateTime>("EndDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EpsBasic")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EpsDiluted")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FinancingCashFlow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FreeCashFlow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("GrossProfit")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Inventory")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("InvestingCashFlow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("LongTermDebt")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("NetIncome")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OperatingCashFlow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OperatingExpenses")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OperatingIncome")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("PeriodType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalCurrentAssets")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalLiabilities")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalNonCurrentAssets")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalRevenue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalStockholdersEquity")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("Isin", "PeriodType", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("FinancialStatements");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.ForwardEstimateEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ExpectedEps")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ExpectedGrowthRate")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ExpectedRevenue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Period")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("Isin", "Period")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ForwardEstimates");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.FundamentalsSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("CacheTtlHours")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableYahooFallback")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.TickerFundamentalsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Ticker")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal>("CurrentPrice")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CurrentRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal>("DayChangeAbsolute")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal>("DayChangePercent")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("DebtToEquity")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("DividendYield")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal>("EnterpriseValue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EvToEbitda")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EvToRevenue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExDividendDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Exchange")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal>("FiftyTwoWeekHigh")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal>("FiftyTwoWeekLow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("GrossMargin")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("InterestCoverage")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal>("MarketCapitalization")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("NetProfitMargin")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OperatingMargin")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PayoutRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PbRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PeRatioForward")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PeRatioTrailing")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PegRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PsRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("QuickRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ReturnOnAssets")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ReturnOnEquity")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ReturnOnInvestedCapital")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("TradingCurrency")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Ticker");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.ToTable("TickerFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.CompanyExecutiveEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetFundamentalsEntity", "AssetFundamentals")
|
||||||
|
.WithMany("Executives")
|
||||||
|
.HasForeignKey("Isin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssetFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.FinancialStatementEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetFundamentalsEntity", "AssetFundamentals")
|
||||||
|
.WithMany("FinancialStatements")
|
||||||
|
.HasForeignKey("Isin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssetFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.ForwardEstimateEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetFundamentalsEntity", "AssetFundamentals")
|
||||||
|
.WithMany("Estimates")
|
||||||
|
.HasForeignKey("Isin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssetFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.TickerFundamentalsEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetFundamentalsEntity", "AssetFundamentals")
|
||||||
|
.WithMany("TickerFundamentals")
|
||||||
|
.HasForeignKey("Isin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssetFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetFundamentalsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Estimates");
|
||||||
|
|
||||||
|
b.Navigation("Executives");
|
||||||
|
|
||||||
|
b.Navigation("FinancialStatements");
|
||||||
|
|
||||||
|
b.Navigation("TickerFundamentals");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Init : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AssetFundamentals",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Isin = table.Column<string>(type: "text", nullable: false),
|
||||||
|
PrimaryTicker = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CompanyName = table.Column<string>(type: "text", nullable: false),
|
||||||
|
BusinessSummary = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Sector = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Industry = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Country = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Employees = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
PercentHeldByInstitutions = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
PercentHeldByInsiders = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
ShortRatio = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
ShortPercentOfFloat = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
ConsensusRating = table.Column<string>(type: "text", nullable: true),
|
||||||
|
PriceTargetLow = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
PriceTargetHigh = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
PriceTargetMedian = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
PriceTargetMean = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
ExDividendDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
NextEarningsDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
LastUpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
LastStaticUpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AssetFundamentals", x => x.Isin);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Settings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CacheTtlHours = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
EnableYahooFallback = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Settings", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CompanyExecutives",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Isin = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Age = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
Compensation = table.Column<decimal>(type: "numeric", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_CompanyExecutives", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_CompanyExecutives_AssetFundamentals_Isin",
|
||||||
|
column: x => x.Isin,
|
||||||
|
principalTable: "AssetFundamentals",
|
||||||
|
principalColumn: "Isin",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FinancialStatements",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Isin = table.Column<string>(type: "text", nullable: false),
|
||||||
|
PeriodType = table.Column<string>(type: "text", nullable: false),
|
||||||
|
EndDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
TotalRevenue = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
CostOfRevenue = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
GrossProfit = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
OperatingExpenses = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
OperatingIncome = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
Ebitda = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
NetIncome = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
EpsBasic = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
EpsDiluted = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
CashAndCashEquivalents = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
AccountsReceivable = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
Inventory = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
TotalCurrentAssets = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
TotalNonCurrentAssets = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
CurrentLiabilities = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
LongTermDebt = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
TotalLiabilities = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
TotalStockholdersEquity = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
OperatingCashFlow = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
InvestingCashFlow = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
CapitalExpenditures = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
FinancingCashFlow = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
FreeCashFlow = table.Column<decimal>(type: "numeric", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FinancialStatements", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FinancialStatements_AssetFundamentals_Isin",
|
||||||
|
column: x => x.Isin,
|
||||||
|
principalTable: "AssetFundamentals",
|
||||||
|
principalColumn: "Isin",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ForwardEstimates",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Isin = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Period = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ExpectedRevenue = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
ExpectedEps = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
ExpectedGrowthRate = table.Column<decimal>(type: "numeric", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ForwardEstimates", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ForwardEstimates_AssetFundamentals_Isin",
|
||||||
|
column: x => x.Isin,
|
||||||
|
principalTable: "AssetFundamentals",
|
||||||
|
principalColumn: "Isin",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TickerFundamentals",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Ticker = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Isin = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Exchange = table.Column<string>(type: "text", nullable: true),
|
||||||
|
TradingCurrency = table.Column<string>(type: "text", nullable: true),
|
||||||
|
CurrentPrice = table.Column<decimal>(type: "numeric", nullable: false),
|
||||||
|
DayChangeAbsolute = table.Column<decimal>(type: "numeric", nullable: false),
|
||||||
|
DayChangePercent = table.Column<decimal>(type: "numeric", nullable: false),
|
||||||
|
FiftyTwoWeekHigh = table.Column<decimal>(type: "numeric", nullable: false),
|
||||||
|
FiftyTwoWeekLow = table.Column<decimal>(type: "numeric", nullable: false),
|
||||||
|
MarketCapitalization = table.Column<decimal>(type: "numeric", nullable: false),
|
||||||
|
EnterpriseValue = table.Column<decimal>(type: "numeric", nullable: false),
|
||||||
|
PeRatioTrailing = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
PeRatioForward = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
PegRatio = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
PbRatio = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
PsRatio = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
EvToEbitda = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
EvToRevenue = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
GrossMargin = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
OperatingMargin = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
NetProfitMargin = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
ReturnOnEquity = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
ReturnOnAssets = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
ReturnOnInvestedCapital = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
DebtToEquity = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
CurrentRatio = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
QuickRatio = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
InterestCoverage = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
DividendYield = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
PayoutRatio = table.Column<decimal>(type: "numeric", nullable: true),
|
||||||
|
ExDividendDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
LastUpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TickerFundamentals", x => x.Ticker);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TickerFundamentals_AssetFundamentals_Isin",
|
||||||
|
column: x => x.Isin,
|
||||||
|
principalTable: "AssetFundamentals",
|
||||||
|
principalColumn: "Isin",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AssetFundamentals_PrimaryTicker",
|
||||||
|
table: "AssetFundamentals",
|
||||||
|
column: "PrimaryTicker",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_CompanyExecutives_Isin",
|
||||||
|
table: "CompanyExecutives",
|
||||||
|
column: "Isin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FinancialStatements_Isin",
|
||||||
|
table: "FinancialStatements",
|
||||||
|
column: "Isin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FinancialStatements_Isin_PeriodType_EndDate",
|
||||||
|
table: "FinancialStatements",
|
||||||
|
columns: new[] { "Isin", "PeriodType", "EndDate" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ForwardEstimates_Isin",
|
||||||
|
table: "ForwardEstimates",
|
||||||
|
column: "Isin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ForwardEstimates_Isin_Period",
|
||||||
|
table: "ForwardEstimates",
|
||||||
|
columns: new[] { "Isin", "Period" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TickerFundamentals_Isin",
|
||||||
|
table: "TickerFundamentals",
|
||||||
|
column: "Isin");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CompanyExecutives");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FinancialStatements");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ForwardEstimates");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TickerFundamentals");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AssetFundamentals");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FinlyticFundamentals.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(FundamentalsDbContext))]
|
||||||
|
partial class FundamentalsDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetFundamentalsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("BusinessSummary")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ConsensusRating")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Country")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int?>("Employees")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExDividendDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Industry")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastStaticUpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("NextEarningsDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PercentHeldByInsiders")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PercentHeldByInstitutions")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetHigh")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetLow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetMean")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetMedian")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("PrimaryTicker")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Sector")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ShortPercentOfFloat")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ShortRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.HasKey("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("PrimaryTicker")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AssetFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.CompanyExecutiveEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int?>("Age")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Compensation")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.ToTable("CompanyExecutives");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.FinancialStatementEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("AccountsReceivable")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CapitalExpenditures")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CashAndCashEquivalents")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CostOfRevenue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CurrentLiabilities")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Ebitda")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<DateTime>("EndDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EpsBasic")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EpsDiluted")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FinancingCashFlow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FreeCashFlow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("GrossProfit")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Inventory")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("InvestingCashFlow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("LongTermDebt")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("NetIncome")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OperatingCashFlow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OperatingExpenses")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OperatingIncome")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("PeriodType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalCurrentAssets")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalLiabilities")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalNonCurrentAssets")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalRevenue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalStockholdersEquity")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("Isin", "PeriodType", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("FinancialStatements");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.ForwardEstimateEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ExpectedEps")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ExpectedGrowthRate")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ExpectedRevenue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Period")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("Isin", "Period")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ForwardEstimates");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.FundamentalsSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("CacheTtlHours")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableYahooFallback")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.TickerFundamentalsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Ticker")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal>("CurrentPrice")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CurrentRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal>("DayChangeAbsolute")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal>("DayChangePercent")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("DebtToEquity")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("DividendYield")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal>("EnterpriseValue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EvToEbitda")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EvToRevenue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExDividendDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Exchange")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal>("FiftyTwoWeekHigh")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal>("FiftyTwoWeekLow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("GrossMargin")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("InterestCoverage")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal>("MarketCapitalization")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("NetProfitMargin")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OperatingMargin")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PayoutRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PbRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PeRatioForward")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PeRatioTrailing")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PegRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PsRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("QuickRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ReturnOnAssets")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ReturnOnEquity")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ReturnOnInvestedCapital")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("TradingCurrency")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Ticker");
|
||||||
|
|
||||||
|
b.HasIndex("Isin");
|
||||||
|
|
||||||
|
b.ToTable("TickerFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.CompanyExecutiveEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetFundamentalsEntity", "AssetFundamentals")
|
||||||
|
.WithMany("Executives")
|
||||||
|
.HasForeignKey("Isin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssetFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.FinancialStatementEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetFundamentalsEntity", "AssetFundamentals")
|
||||||
|
.WithMany("FinancialStatements")
|
||||||
|
.HasForeignKey("Isin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssetFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.ForwardEstimateEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetFundamentalsEntity", "AssetFundamentals")
|
||||||
|
.WithMany("Estimates")
|
||||||
|
.HasForeignKey("Isin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssetFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.TickerFundamentalsEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetFundamentalsEntity", "AssetFundamentals")
|
||||||
|
.WithMany("TickerFundamentals")
|
||||||
|
.HasForeignKey("Isin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssetFundamentals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetFundamentalsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Estimates");
|
||||||
|
|
||||||
|
b.Navigation("Executives");
|
||||||
|
|
||||||
|
b.Navigation("FinancialStatements");
|
||||||
|
|
||||||
|
b.Navigation("TickerFundamentals");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using FinlyticFundamentals.Database;
|
||||||
|
using FinlyticFundamentals.Services;
|
||||||
|
using FinlyticFundamentals.Util;
|
||||||
|
|
||||||
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
|
// Register DB Context
|
||||||
|
builder.Services.AddDbContext<FundamentalsDbContext>(options =>
|
||||||
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
|
// Register HTTP Clients
|
||||||
|
builder.Services.AddHttpClient<IYahooFinanceScraper, YahooFinanceScraper>()
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
||||||
|
{
|
||||||
|
UseCookies = true,
|
||||||
|
CookieContainer = new System.Net.CookieContainer()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Application Services
|
||||||
|
builder.Services.AddSingleton<FinlyticCore.Services.Yahoo.YahooFinanceClient>();
|
||||||
|
builder.Services.AddTransient<IYahooFinanceScraper, YahooFinanceScraper>();
|
||||||
|
builder.Services.AddSingleton<IFundamentalsDbService, FundamentalsDbService>();
|
||||||
|
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
||||||
|
|
||||||
|
// Register MQTT Client (as a Hosted Service)
|
||||||
|
builder.Services.AddHostedService<FundamentalsMqttClient>();
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
// Run startup database migrations
|
||||||
|
using (var scope = host.Services.CreateScope())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<FundamentalsDbContext>();
|
||||||
|
await context.Database.MigrateAsync();
|
||||||
|
Console.WriteLine("Database migrations successfully executed for FinlyticFundamentals.");
|
||||||
|
|
||||||
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
||||||
|
await settingsService.GetSettingsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
|
||||||
|
logger.LogError(ex, "[{Channel}] An error occurred during database migration on startup.", "FundamentalsChannel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await host.RunAsync();
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Finlytic Fundamentals Service
|
||||||
|
|
||||||
|
Finlytic Fundamentals is a C# background worker microservice responsible for fetching, caching, and serving financial fundamental data (P/E ratios, market cap, dividend yield, revenue growth, corporate calendar events) across global equities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Features & Architecture
|
||||||
|
|
||||||
|
1. **Fundamental Data Ingestion**:
|
||||||
|
- Scrapes and ingests company fundamentals (`AssetFundamentalsDto`) including P/E, EPS, Market Cap, Dividend Yield, Revenue, Profit Margins, and Debt-to-Equity ratios.
|
||||||
|
|
||||||
|
2. **Corporate Event Calendar**:
|
||||||
|
- Tracks earnings release dates, ex-dividend dates, payout dates, and shareholder meetings (`CorporateEventDto`).
|
||||||
|
|
||||||
|
3. **MQTT Distribution Channels**:
|
||||||
|
- Publishes fundamental updates to `finlytic/fundamentals/{isin}` and `finlytic/assets/fundamentals/{isin}`.
|
||||||
|
- Responds to RPC requests on `services/request/fundamentals_Get/#` and `services/request/events_GetAll/#`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Status
|
||||||
|
|
||||||
|
### Implemented Features
|
||||||
|
- [x] Fundamentals Database Persistence & Caching (`FundamentalsDbContext`).
|
||||||
|
- [x] Corporate Event Calendar storage & query handlers.
|
||||||
|
- [x] Zero-Allocation MQTT serialization via `FinlyticJsonSerializerContext`.
|
||||||
|
- [x] Pure Worker Service architecture (`Host.CreateApplicationBuilder`, no Kestrel HTTP server).
|
||||||
|
|
||||||
|
### Planned Features
|
||||||
|
- [ ] Financial Modeling Prep / SEC EDGAR API automated quarterly filing sync.
|
||||||
|
- [ ] Automated Dividend Growth Rate & Dividend Safety Rating calculation engine.
|
||||||
@@ -0,0 +1,589 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FinlyticCore.Dtos.Fundamentals;
|
||||||
|
using FinlyticCore.Services.Yahoo;
|
||||||
|
using FinlyticFundamentals.Database;
|
||||||
|
using FinlyticFundamentals.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Services;
|
||||||
|
|
||||||
|
public interface IFundamentalsDbService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the fundamental data for a given ISIN.
|
||||||
|
/// If a specific ticker is provided, the resolution pipeline prioritizes/fetches only that ticker.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="isin">The ISIN identifier of the asset.</param>
|
||||||
|
/// <param name="ticker">Optional specific ticker symbol (e.g., "APC.DE"). If omitted, tickers are resolved automatically.</param>
|
||||||
|
/// <param name="forceRefresh">If true, forces a full static scrape for profile, financials, and executives.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The mapped <see cref="AssetFundamentalsDto"/> or null if unavailable.</returns>
|
||||||
|
Task<AssetFundamentalsDto?> GetFundamentalsAsync(
|
||||||
|
string isin,
|
||||||
|
string? ticker = null,
|
||||||
|
bool forceRefresh = false,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all upcoming and historic corporate events (e.g., earnings releases, ex-dividend dates).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>A list of corporate events sorted chronologically.</returns>
|
||||||
|
Task<List<CorporateEventDto>> GetAllEventsAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FundamentalsDbService : IFundamentalsDbService
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> IsinLocks = new();
|
||||||
|
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly IYahooFinanceScraper _scraper;
|
||||||
|
private readonly YahooFinanceClient _yahooClient;
|
||||||
|
private readonly ILogger<FundamentalsDbService> _logger;
|
||||||
|
|
||||||
|
public FundamentalsDbService(
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
IYahooFinanceScraper scraper,
|
||||||
|
YahooFinanceClient yahooClient,
|
||||||
|
ILogger<FundamentalsDbService> logger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_scraper = scraper;
|
||||||
|
_yahooClient = yahooClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<AssetFundamentalsDto?> GetFundamentalsAsync(
|
||||||
|
string isin,
|
||||||
|
string? ticker = null,
|
||||||
|
bool forceRefresh = false,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(isin)) return null;
|
||||||
|
var cleanIsin = isin.Trim().ToUpperInvariant();
|
||||||
|
var requestedTicker = ticker?.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<FundamentalsDbContext>();
|
||||||
|
|
||||||
|
var isinLock = IsinLocks.GetOrAdd(cleanIsin, _ => new SemaphoreSlim(1, 1));
|
||||||
|
await isinLock.WaitAsync(cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. Aus DB laden
|
||||||
|
var entity = await LoadEntityGraphAsync(context, cleanIsin, cancellationToken);
|
||||||
|
|
||||||
|
// Statische Daten älter als 30 Tage oder forced?
|
||||||
|
bool needsStaticScrape = entity == null || forceRefresh || (DateTime.UtcNow - entity.LastStaticUpdatedAt).TotalDays > 30;
|
||||||
|
|
||||||
|
if (needsStaticScrape)
|
||||||
|
{
|
||||||
|
entity = await ExecuteFullScrapeAndPersistAsync(context, cleanIsin, requestedTicker, entity, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Statik ist frisch -> Prüfen ob requested Ticker existiert oder neu nachgeladen werden muss
|
||||||
|
entity = await EnsureTickerDataUpToDateAsync(context, cleanIsin, requestedTicker, entity!, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity != null ? MapToDto(entity, requestedTicker) : null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{Channel}] Failed to process fundamentals for ISIN {Isin}", "FundamentalsChannel", cleanIsin);
|
||||||
|
|
||||||
|
// Fallback auf Datenbankstand, falls vorhanden
|
||||||
|
var fallback = await LoadEntityGraphAsync(context, cleanIsin, cancellationToken);
|
||||||
|
return fallback != null ? MapToDto(fallback, requestedTicker) : null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isinLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Internal Logic Pipelines
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stellt sicher, dass der angeforderte Ticker existiert und dessen Live-Preise frisch sind (TTL: 15 Minuten).
|
||||||
|
/// </summary>
|
||||||
|
private async Task<AssetFundamentalsEntity> EnsureTickerDataUpToDateAsync(
|
||||||
|
FundamentalsDbContext context,
|
||||||
|
string isin,
|
||||||
|
string? requestedTicker,
|
||||||
|
AssetFundamentalsEntity entity,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var targetTickerSymbol = requestedTicker
|
||||||
|
?? (entity.TickerFundamentals.FirstOrDefault(t => t.Ticker == entity.PrimaryTicker)?.Ticker
|
||||||
|
?? entity.TickerFundamentals.FirstOrDefault()?.Ticker);
|
||||||
|
|
||||||
|
// Fall A: Ticker noch gar nicht in DB -> Einzel-Scrape für diesen Ticker durchführen
|
||||||
|
if (!string.IsNullOrEmpty(targetTickerSymbol) && !entity.TickerFundamentals.Any(t => t.Ticker.Equals(targetTickerSymbol, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] Targeted ticker '{Ticker}' missing in DB for ISIN {Isin}. Fetching on-demand...", "FundamentalsChannel", targetTickerSymbol, isin);
|
||||||
|
var scraped = await _scraper.ScrapeFundamentalsAsync(isin, targetTickerSymbol, cancellationToken);
|
||||||
|
if (scraped?.TickerData != null)
|
||||||
|
{
|
||||||
|
entity.TickerFundamentals.Add(scraped.TickerData);
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall B: Ticker existiert -> Prüfen ob Live-Kurs älter als 15 Minuten ist
|
||||||
|
var targetTickerEntity = entity.TickerFundamentals.FirstOrDefault(t => t.Ticker.Equals(targetTickerSymbol, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (targetTickerEntity != null && (DateTime.UtcNow - targetTickerEntity.LastUpdatedAt).TotalMinutes > 15)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] Quote expired for ticker '{Ticker}'. Refreshing live price...", "FundamentalsChannel", targetTickerSymbol);
|
||||||
|
var quotesResponse = await _yahooClient.GetQuotesAsync(new[] { targetTickerEntity.Ticker }, cancellationToken);
|
||||||
|
var liveQuote = quotesResponse?.QuoteResponse?.Result?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (liveQuote != null)
|
||||||
|
{
|
||||||
|
targetTickerEntity.CurrentPrice = (decimal?)liveQuote.RegularMarketPrice ?? targetTickerEntity.CurrentPrice;
|
||||||
|
targetTickerEntity.DayChangeAbsolute = (decimal?)liveQuote.RegularMarketChange ?? targetTickerEntity.DayChangeAbsolute;
|
||||||
|
targetTickerEntity.DayChangePercent = (decimal?)liveQuote.RegularMarketChangePercent ?? targetTickerEntity.DayChangePercent;
|
||||||
|
targetTickerEntity.FiftyTwoWeekHigh = (decimal?)liveQuote.FiftyTwoWeekHigh ?? targetTickerEntity.FiftyTwoWeekHigh;
|
||||||
|
targetTickerEntity.FiftyTwoWeekLow = (decimal?)liveQuote.FiftyTwoWeekLow ?? targetTickerEntity.FiftyTwoWeekLow;
|
||||||
|
targetTickerEntity.MarketCapitalization = (decimal?)liveQuote.MarketCap ?? targetTickerEntity.MarketCapitalization;
|
||||||
|
targetTickerEntity.LastUpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
entity.LastUpdatedAt = DateTime.UtcNow;
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Führt ein vollständiges Scraping der Bilanzen und Ticker durch und speichert das Ergebnis ab.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<AssetFundamentalsEntity?> ExecuteFullScrapeAndPersistAsync(
|
||||||
|
FundamentalsDbContext context,
|
||||||
|
string isin,
|
||||||
|
string? requestedTicker,
|
||||||
|
AssetFundamentalsEntity? existingEntity,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] Initiating full static scrape for ISIN {Isin}...", "FundamentalsChannel", isin);
|
||||||
|
|
||||||
|
List<string> tickers = new();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(requestedTicker))
|
||||||
|
{
|
||||||
|
tickers.Add(requestedTicker);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tickers = await _scraper.ResolveAllTickersFromIsinAsync(isin, cancellationToken);
|
||||||
|
if (existingEntity?.TickerFundamentals != null)
|
||||||
|
{
|
||||||
|
foreach (var tf in existingEntity.TickerFundamentals)
|
||||||
|
{
|
||||||
|
if (!tickers.Contains(tf.Ticker, StringComparer.OrdinalIgnoreCase))
|
||||||
|
tickers.Add(tf.Ticker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tickers.Count == 0) return existingEntity;
|
||||||
|
|
||||||
|
var primaryTicker = tickers[0];
|
||||||
|
var scraped = await _scraper.ScrapeFundamentalsAsync(isin, primaryTicker, cancellationToken);
|
||||||
|
if (scraped == null) return existingEntity;
|
||||||
|
|
||||||
|
var tickerEntities = new List<TickerFundamentalsEntity> { scraped.TickerData };
|
||||||
|
|
||||||
|
// Sekundär-Ticker parallel laden (nur wenn kein spezifischer Ticker verlangt war)
|
||||||
|
if (string.IsNullOrWhiteSpace(requestedTicker) && tickers.Count > 1)
|
||||||
|
{
|
||||||
|
var altTasks = tickers.Skip(1).Take(4).Select(async alt =>
|
||||||
|
{
|
||||||
|
try { return await _scraper.ScrapeFundamentalsAsync(isin, alt, cancellationToken); }
|
||||||
|
catch { return null; }
|
||||||
|
});
|
||||||
|
|
||||||
|
var altResults = await Task.WhenAll(altTasks);
|
||||||
|
foreach (var alt in altResults)
|
||||||
|
{
|
||||||
|
if (alt?.TickerData != null) tickerEntities.Add(alt.TickerData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB Upsert
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SaveOrUpdateFundamentalsAsync(context, isin, primaryTicker, scraped, tickerEntities, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex) when (ex.InnerException is Npgsql.NpgsqlException npgEx && npgEx.SqlState == "23505")
|
||||||
|
{
|
||||||
|
context.ChangeTracker.Clear();
|
||||||
|
await SaveOrUpdateFundamentalsAsync(context, isin, primaryTicker, scraped, tickerEntities, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await LoadEntityGraphAsync(context, isin, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Data Access & Mapping Helpers
|
||||||
|
|
||||||
|
private static Task<AssetFundamentalsEntity?> LoadEntityGraphAsync(FundamentalsDbContext context, string isin, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return context.AssetFundamentals
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(f => f.Executives)
|
||||||
|
.Include(f => f.FinancialStatements)
|
||||||
|
.Include(f => f.Estimates)
|
||||||
|
.Include(f => f.TickerFundamentals)
|
||||||
|
.FirstOrDefaultAsync(f => f.Isin == isin, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveOrUpdateFundamentalsAsync(
|
||||||
|
FundamentalsDbContext context,
|
||||||
|
string isin,
|
||||||
|
string primaryTicker,
|
||||||
|
ScrapedFundamentalsData scraped,
|
||||||
|
List<TickerFundamentalsEntity> tickerEntities,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await context.AssetFundamentals.FirstOrDefaultAsync(f => f.Isin == isin, cancellationToken);
|
||||||
|
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
entity = scraped.Fundamentals;
|
||||||
|
entity.Isin = isin;
|
||||||
|
entity.PrimaryTicker = primaryTicker;
|
||||||
|
entity.Executives = scraped.Executives;
|
||||||
|
entity.FinancialStatements = scraped.Statements;
|
||||||
|
entity.Estimates = scraped.Estimates;
|
||||||
|
entity.TickerFundamentals = new List<TickerFundamentalsEntity>();
|
||||||
|
|
||||||
|
foreach (var ex in entity.Executives) { ex.Isin = isin; if (ex.Id == Guid.Empty) ex.Id = Guid.NewGuid(); }
|
||||||
|
foreach (var stmt in entity.FinancialStatements) { stmt.Isin = isin; if (stmt.Id == Guid.Empty) stmt.Id = Guid.NewGuid(); }
|
||||||
|
|
||||||
|
context.AssetFundamentals.Add(entity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entity.PrimaryTicker = primaryTicker;
|
||||||
|
entity.CompanyName = !string.IsNullOrWhiteSpace(scraped.Fundamentals.CompanyName) ? scraped.Fundamentals.CompanyName : entity.CompanyName;
|
||||||
|
entity.BusinessSummary = !string.IsNullOrWhiteSpace(scraped.Fundamentals.BusinessSummary) ? scraped.Fundamentals.BusinessSummary : entity.BusinessSummary;
|
||||||
|
entity.Sector = !string.IsNullOrWhiteSpace(scraped.Fundamentals.Sector) ? scraped.Fundamentals.Sector : entity.Sector;
|
||||||
|
entity.Industry = !string.IsNullOrWhiteSpace(scraped.Fundamentals.Industry) ? scraped.Fundamentals.Industry : entity.Industry;
|
||||||
|
entity.Country = !string.IsNullOrWhiteSpace(scraped.Fundamentals.Country) ? scraped.Fundamentals.Country : entity.Country;
|
||||||
|
entity.Employees = scraped.Fundamentals.Employees ?? entity.Employees;
|
||||||
|
|
||||||
|
entity.PercentHeldByInstitutions = scraped.Fundamentals.PercentHeldByInstitutions ?? entity.PercentHeldByInstitutions;
|
||||||
|
entity.PercentHeldByInsiders = scraped.Fundamentals.PercentHeldByInsiders ?? entity.PercentHeldByInsiders;
|
||||||
|
entity.ShortRatio = scraped.Fundamentals.ShortRatio ?? entity.ShortRatio;
|
||||||
|
entity.ShortPercentOfFloat = scraped.Fundamentals.ShortPercentOfFloat ?? entity.ShortPercentOfFloat;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(scraped.Fundamentals.ConsensusRating) && !scraped.Fundamentals.ConsensusRating.Equals("none", StringComparison.OrdinalIgnoreCase))
|
||||||
|
entity.ConsensusRating = scraped.Fundamentals.ConsensusRating;
|
||||||
|
|
||||||
|
entity.PriceTargetLow = scraped.Fundamentals.PriceTargetLow ?? entity.PriceTargetLow;
|
||||||
|
entity.PriceTargetHigh = scraped.Fundamentals.PriceTargetHigh ?? entity.PriceTargetHigh;
|
||||||
|
entity.PriceTargetMedian = scraped.Fundamentals.PriceTargetMedian ?? entity.PriceTargetMedian;
|
||||||
|
entity.PriceTargetMean = scraped.Fundamentals.PriceTargetMean ?? entity.PriceTargetMean;
|
||||||
|
|
||||||
|
entity.ExDividendDate = scraped.Fundamentals.ExDividendDate ?? entity.ExDividendDate;
|
||||||
|
entity.NextEarningsDate = scraped.Fundamentals.NextEarningsDate ?? entity.NextEarningsDate;
|
||||||
|
entity.LastStaticUpdatedAt = DateTime.UtcNow;
|
||||||
|
entity.LastUpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Executives & Statements aktualisieren
|
||||||
|
if (scraped.Executives.Count > 0)
|
||||||
|
{
|
||||||
|
await context.CompanyExecutives.Where(e => e.Isin == isin).ExecuteDeleteAsync(cancellationToken);
|
||||||
|
foreach (var exec in scraped.Executives)
|
||||||
|
{
|
||||||
|
exec.Isin = isin;
|
||||||
|
if (exec.Id == Guid.Empty) exec.Id = Guid.NewGuid();
|
||||||
|
context.CompanyExecutives.Add(exec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scraped.Statements.Count > 0)
|
||||||
|
{
|
||||||
|
var existingStmts = await context.FinancialStatements.Where(s => s.Isin == isin).ToListAsync(cancellationToken);
|
||||||
|
foreach (var stmt in scraped.Statements)
|
||||||
|
{
|
||||||
|
var existingStmt = existingStmts.FirstOrDefault(s => s.PeriodType == stmt.PeriodType && s.EndDate.Date == stmt.EndDate.Date);
|
||||||
|
if (existingStmt == null)
|
||||||
|
{
|
||||||
|
stmt.Isin = isin;
|
||||||
|
if (stmt.Id == Guid.Empty) stmt.Id = Guid.NewGuid();
|
||||||
|
context.FinancialStatements.Add(stmt);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingStmt.TotalRevenue = stmt.TotalRevenue ?? existingStmt.TotalRevenue;
|
||||||
|
existingStmt.CostOfRevenue = stmt.CostOfRevenue ?? existingStmt.CostOfRevenue;
|
||||||
|
existingStmt.GrossProfit = stmt.GrossProfit ?? existingStmt.GrossProfit;
|
||||||
|
existingStmt.OperatingExpenses = stmt.OperatingExpenses ?? existingStmt.OperatingExpenses;
|
||||||
|
existingStmt.OperatingIncome = stmt.OperatingIncome ?? existingStmt.OperatingIncome;
|
||||||
|
existingStmt.Ebitda = stmt.Ebitda ?? existingStmt.Ebitda;
|
||||||
|
existingStmt.NetIncome = stmt.NetIncome ?? existingStmt.NetIncome;
|
||||||
|
existingStmt.CashAndCashEquivalents = stmt.CashAndCashEquivalents ?? existingStmt.CashAndCashEquivalents;
|
||||||
|
existingStmt.TotalCurrentAssets = stmt.TotalCurrentAssets ?? existingStmt.TotalCurrentAssets;
|
||||||
|
existingStmt.CurrentLiabilities = stmt.CurrentLiabilities ?? existingStmt.CurrentLiabilities;
|
||||||
|
existingStmt.LongTermDebt = stmt.LongTermDebt ?? existingStmt.LongTermDebt;
|
||||||
|
existingStmt.TotalLiabilities = stmt.TotalLiabilities ?? existingStmt.TotalLiabilities;
|
||||||
|
existingStmt.TotalStockholdersEquity = stmt.TotalStockholdersEquity ?? existingStmt.TotalStockholdersEquity;
|
||||||
|
existingStmt.OperatingCashFlow = stmt.OperatingCashFlow ?? existingStmt.OperatingCashFlow;
|
||||||
|
existingStmt.InvestingCashFlow = stmt.InvestingCashFlow ?? existingStmt.InvestingCashFlow;
|
||||||
|
existingStmt.CapitalExpenditures = stmt.CapitalExpenditures ?? existingStmt.CapitalExpenditures;
|
||||||
|
existingStmt.FinancingCashFlow = stmt.FinancingCashFlow ?? existingStmt.FinancingCashFlow;
|
||||||
|
existingStmt.FreeCashFlow = stmt.FreeCashFlow ?? existingStmt.FreeCashFlow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ticker-Fundamentaldaten aktualisieren
|
||||||
|
foreach (var t in tickerEntities)
|
||||||
|
{
|
||||||
|
t.Isin = isin;
|
||||||
|
var existingTicker = await context.TickerFundamentals.FirstOrDefaultAsync(tf => tf.Ticker == t.Ticker, cancellationToken);
|
||||||
|
|
||||||
|
if (existingTicker == null)
|
||||||
|
{
|
||||||
|
context.TickerFundamentals.Add(t);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingTicker.Exchange = !string.IsNullOrEmpty(t.Exchange) ? t.Exchange : existingTicker.Exchange;
|
||||||
|
existingTicker.TradingCurrency = !string.IsNullOrEmpty(t.TradingCurrency) ? t.TradingCurrency : existingTicker.TradingCurrency;
|
||||||
|
existingTicker.CurrentPrice = t.CurrentPrice > 0 ? t.CurrentPrice : existingTicker.CurrentPrice;
|
||||||
|
existingTicker.DayChangeAbsolute = t.DayChangeAbsolute != 0 ? t.DayChangeAbsolute : existingTicker.DayChangeAbsolute;
|
||||||
|
existingTicker.DayChangePercent = t.DayChangePercent != 0 ? t.DayChangePercent : existingTicker.DayChangePercent;
|
||||||
|
existingTicker.FiftyTwoWeekHigh = t.FiftyTwoWeekHigh > 0 ? t.FiftyTwoWeekHigh : existingTicker.FiftyTwoWeekHigh;
|
||||||
|
existingTicker.FiftyTwoWeekLow = t.FiftyTwoWeekLow > 0 ? t.FiftyTwoWeekLow : existingTicker.FiftyTwoWeekLow;
|
||||||
|
existingTicker.MarketCapitalization = t.MarketCapitalization > 0 ? t.MarketCapitalization : existingTicker.MarketCapitalization;
|
||||||
|
existingTicker.EnterpriseValue = t.EnterpriseValue > 0 ? t.EnterpriseValue : existingTicker.EnterpriseValue;
|
||||||
|
existingTicker.PeRatioTrailing = t.PeRatioTrailing ?? existingTicker.PeRatioTrailing;
|
||||||
|
existingTicker.PeRatioForward = t.PeRatioForward ?? existingTicker.PeRatioForward;
|
||||||
|
existingTicker.PegRatio = t.PegRatio ?? existingTicker.PegRatio;
|
||||||
|
existingTicker.PbRatio = t.PbRatio ?? existingTicker.PbRatio;
|
||||||
|
existingTicker.PsRatio = t.PsRatio ?? existingTicker.PsRatio;
|
||||||
|
existingTicker.EvToEbitda = t.EvToEbitda ?? existingTicker.EvToEbitda;
|
||||||
|
existingTicker.EvToRevenue = t.EvToRevenue ?? existingTicker.EvToRevenue;
|
||||||
|
existingTicker.GrossMargin = t.GrossMargin ?? existingTicker.GrossMargin;
|
||||||
|
existingTicker.OperatingMargin = t.OperatingMargin ?? existingTicker.OperatingMargin;
|
||||||
|
existingTicker.NetProfitMargin = t.NetProfitMargin ?? existingTicker.NetProfitMargin;
|
||||||
|
existingTicker.ReturnOnEquity = t.ReturnOnEquity ?? existingTicker.ReturnOnEquity;
|
||||||
|
existingTicker.ReturnOnAssets = t.ReturnOnAssets ?? existingTicker.ReturnOnAssets;
|
||||||
|
existingTicker.DebtToEquity = t.DebtToEquity ?? existingTicker.DebtToEquity;
|
||||||
|
existingTicker.CurrentRatio = t.CurrentRatio ?? existingTicker.CurrentRatio;
|
||||||
|
existingTicker.QuickRatio = t.QuickRatio ?? existingTicker.QuickRatio;
|
||||||
|
existingTicker.DividendYield = t.DividendYield ?? existingTicker.DividendYield;
|
||||||
|
existingTicker.PayoutRatio = t.PayoutRatio ?? existingTicker.PayoutRatio;
|
||||||
|
existingTicker.ExDividendDate = t.ExDividendDate ?? existingTicker.ExDividendDate;
|
||||||
|
existingTicker.LastUpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AssetFundamentalsDto MapToDto(AssetFundamentalsEntity entity, string? requestedTicker)
|
||||||
|
{
|
||||||
|
var targetTicker = entity.TickerFundamentals?.FirstOrDefault(t => t.Ticker.Equals(requestedTicker, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? entity.TickerFundamentals?.FirstOrDefault(t => t.Ticker.Equals(entity.PrimaryTicker, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? entity.TickerFundamentals?.FirstOrDefault();
|
||||||
|
|
||||||
|
var selectedTickerSymbol = targetTicker?.Ticker ?? requestedTicker ?? entity.PrimaryTicker;
|
||||||
|
|
||||||
|
return new AssetFundamentalsDto
|
||||||
|
{
|
||||||
|
Isin = entity.Isin,
|
||||||
|
PrimaryTicker = entity.PrimaryTicker,
|
||||||
|
Ticker = selectedTickerSymbol,
|
||||||
|
CompanyName = entity.CompanyName,
|
||||||
|
Exchange = targetTicker?.Exchange,
|
||||||
|
TradingCurrency = targetTicker?.TradingCurrency,
|
||||||
|
BusinessSummary = entity.BusinessSummary,
|
||||||
|
Sector = entity.Sector,
|
||||||
|
Industry = entity.Industry,
|
||||||
|
Country = entity.Country,
|
||||||
|
Employees = entity.Employees,
|
||||||
|
|
||||||
|
CurrentPrice = targetTicker?.CurrentPrice ?? 0,
|
||||||
|
DayChangeAbsolute = targetTicker?.DayChangeAbsolute ?? 0,
|
||||||
|
DayChangePercent = targetTicker?.DayChangePercent ?? 0,
|
||||||
|
FiftyTwoWeekHigh = targetTicker?.FiftyTwoWeekHigh ?? 0,
|
||||||
|
FiftyTwoWeekLow = targetTicker?.FiftyTwoWeekLow ?? 0,
|
||||||
|
MarketCapitalization = targetTicker?.MarketCapitalization ?? 0,
|
||||||
|
EnterpriseValue = targetTicker?.EnterpriseValue ?? 0,
|
||||||
|
PeRatioTrailing = targetTicker?.PeRatioTrailing,
|
||||||
|
PeRatioForward = targetTicker?.PeRatioForward,
|
||||||
|
PegRatio = targetTicker?.PegRatio,
|
||||||
|
PbRatio = targetTicker?.PbRatio,
|
||||||
|
PsRatio = targetTicker?.PsRatio,
|
||||||
|
EvToEbitda = targetTicker?.EvToEbitda,
|
||||||
|
EvToRevenue = targetTicker?.EvToRevenue,
|
||||||
|
|
||||||
|
GrossMargin = targetTicker?.GrossMargin,
|
||||||
|
OperatingMargin = targetTicker?.OperatingMargin,
|
||||||
|
NetProfitMargin = targetTicker?.NetProfitMargin,
|
||||||
|
ReturnOnEquity = targetTicker?.ReturnOnEquity,
|
||||||
|
ReturnOnAssets = targetTicker?.ReturnOnAssets,
|
||||||
|
ReturnOnInvestedCapital = targetTicker?.ReturnOnInvestedCapital,
|
||||||
|
DebtToEquity = targetTicker?.DebtToEquity,
|
||||||
|
CurrentRatio = targetTicker?.CurrentRatio,
|
||||||
|
QuickRatio = targetTicker?.QuickRatio,
|
||||||
|
InterestCoverage = targetTicker?.InterestCoverage,
|
||||||
|
|
||||||
|
DividendYield = targetTicker?.DividendYield,
|
||||||
|
PayoutRatio = targetTicker?.PayoutRatio,
|
||||||
|
ExDividendDate = entity.ExDividendDate ?? targetTicker?.ExDividendDate,
|
||||||
|
NextEarningsDate = entity.NextEarningsDate,
|
||||||
|
|
||||||
|
PercentHeldByInstitutions = entity.PercentHeldByInstitutions,
|
||||||
|
PercentHeldByInsiders = entity.PercentHeldByInsiders,
|
||||||
|
ShortRatio = entity.ShortRatio,
|
||||||
|
ShortPercentOfFloat = entity.ShortPercentOfFloat,
|
||||||
|
ConsensusRating = entity.ConsensusRating,
|
||||||
|
PriceTargetLow = entity.PriceTargetLow,
|
||||||
|
PriceTargetHigh = entity.PriceTargetHigh,
|
||||||
|
PriceTargetMedian = entity.PriceTargetMedian,
|
||||||
|
PriceTargetMean = entity.PriceTargetMean,
|
||||||
|
LastUpdatedAt = entity.LastUpdatedAt,
|
||||||
|
|
||||||
|
Executives = entity.Executives.Select(e => new CompanyExecutiveDto
|
||||||
|
{
|
||||||
|
Name = e.Name,
|
||||||
|
Title = e.Title,
|
||||||
|
Age = e.Age,
|
||||||
|
Compensation = e.Compensation
|
||||||
|
}).ToList(),
|
||||||
|
FinancialStatements = entity.FinancialStatements.Select(s => new FinancialStatementDto
|
||||||
|
{
|
||||||
|
PeriodType = s.PeriodType,
|
||||||
|
EndDate = s.EndDate,
|
||||||
|
TotalRevenue = s.TotalRevenue,
|
||||||
|
CostOfRevenue = s.CostOfRevenue,
|
||||||
|
GrossProfit = s.GrossProfit,
|
||||||
|
OperatingExpenses = s.OperatingExpenses,
|
||||||
|
OperatingIncome = s.OperatingIncome,
|
||||||
|
Ebitda = s.Ebitda,
|
||||||
|
NetIncome = s.NetIncome,
|
||||||
|
EpsBasic = s.EpsBasic,
|
||||||
|
EpsDiluted = s.EpsDiluted,
|
||||||
|
CashAndCashEquivalents = s.CashAndCashEquivalents,
|
||||||
|
AccountsReceivable = s.AccountsReceivable,
|
||||||
|
Inventory = s.Inventory,
|
||||||
|
TotalCurrentAssets = s.TotalCurrentAssets,
|
||||||
|
TotalNonCurrentAssets = s.TotalNonCurrentAssets,
|
||||||
|
CurrentLiabilities = s.CurrentLiabilities,
|
||||||
|
LongTermDebt = s.LongTermDebt,
|
||||||
|
TotalLiabilities = s.TotalLiabilities,
|
||||||
|
TotalStockholdersEquity = s.TotalStockholdersEquity,
|
||||||
|
OperatingCashFlow = s.OperatingCashFlow,
|
||||||
|
InvestingCashFlow = s.InvestingCashFlow,
|
||||||
|
CapitalExpenditures = s.CapitalExpenditures,
|
||||||
|
FinancingCashFlow = s.FinancingCashFlow,
|
||||||
|
FreeCashFlow = s.FreeCashFlow
|
||||||
|
}).OrderByDescending(s => s.EndDate).ToList(),
|
||||||
|
Estimates = entity.Estimates.Select(e => new ForwardEstimateDto
|
||||||
|
{
|
||||||
|
Period = e.Period,
|
||||||
|
ExpectedRevenue = e.ExpectedRevenue,
|
||||||
|
ExpectedEps = e.ExpectedEps,
|
||||||
|
ExpectedGrowthRate = e.ExpectedGrowthRate
|
||||||
|
}).ToList(),
|
||||||
|
AvailableTickers = entity.TickerFundamentals.Select(t => new TickerDto
|
||||||
|
{
|
||||||
|
Ticker = t.Ticker,
|
||||||
|
Exchange = t.Exchange,
|
||||||
|
TradingCurrency = t.TradingCurrency,
|
||||||
|
CurrentPrice = t.CurrentPrice,
|
||||||
|
DayChangeAbsolute = t.DayChangeAbsolute,
|
||||||
|
DayChangePercent = t.DayChangePercent,
|
||||||
|
FiftyTwoWeekHigh = t.FiftyTwoWeekHigh,
|
||||||
|
FiftyTwoWeekLow = t.FiftyTwoWeekLow,
|
||||||
|
MarketCapitalization = t.MarketCapitalization,
|
||||||
|
EnterpriseValue = t.EnterpriseValue,
|
||||||
|
PeRatioTrailing = t.PeRatioTrailing,
|
||||||
|
PeRatioForward = t.PeRatioForward,
|
||||||
|
PegRatio = t.PegRatio,
|
||||||
|
PbRatio = t.PbRatio,
|
||||||
|
PsRatio = t.PsRatio,
|
||||||
|
EvToEbitda = t.EvToEbitda,
|
||||||
|
EvToRevenue = t.EvToRevenue,
|
||||||
|
GrossMargin = t.GrossMargin,
|
||||||
|
OperatingMargin = t.OperatingMargin,
|
||||||
|
NetProfitMargin = t.NetProfitMargin,
|
||||||
|
ReturnOnEquity = t.ReturnOnEquity,
|
||||||
|
ReturnOnAssets = t.ReturnOnAssets,
|
||||||
|
ReturnOnInvestedCapital = t.ReturnOnInvestedCapital,
|
||||||
|
DebtToEquity = t.DebtToEquity,
|
||||||
|
CurrentRatio = t.CurrentRatio,
|
||||||
|
QuickRatio = t.QuickRatio,
|
||||||
|
InterestCoverage = t.InterestCoverage,
|
||||||
|
DividendYield = t.DividendYield,
|
||||||
|
PayoutRatio = t.PayoutRatio,
|
||||||
|
ExDividendDate = t.ExDividendDate ?? entity.ExDividendDate
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<CorporateEventDto>> GetAllEventsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<FundamentalsDbContext>();
|
||||||
|
|
||||||
|
var entities = await context.AssetFundamentals
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(f => f.NextEarningsDate.HasValue || f.ExDividendDate.HasValue)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var events = new List<CorporateEventDto>();
|
||||||
|
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
var companyName = string.IsNullOrWhiteSpace(entity.CompanyName) ? entity.PrimaryTicker : entity.CompanyName;
|
||||||
|
|
||||||
|
if (entity.NextEarningsDate.HasValue)
|
||||||
|
{
|
||||||
|
events.Add(new CorporateEventDto
|
||||||
|
{
|
||||||
|
Isin = entity.Isin,
|
||||||
|
Ticker = entity.PrimaryTicker,
|
||||||
|
CompanyName = companyName,
|
||||||
|
EventType = "Quartalsergebnis",
|
||||||
|
Date = entity.NextEarningsDate.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.ExDividendDate.HasValue)
|
||||||
|
{
|
||||||
|
events.Add(new CorporateEventDto
|
||||||
|
{
|
||||||
|
Isin = entity.Isin,
|
||||||
|
Ticker = entity.PrimaryTicker,
|
||||||
|
CompanyName = companyName,
|
||||||
|
EventType = "Ex-Dividendentag",
|
||||||
|
Date = entity.ExDividendDate.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events.OrderBy(e => e.Date).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using FinlyticFundamentals.Database;
|
||||||
|
using FinlyticFundamentals.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Services;
|
||||||
|
|
||||||
|
public interface ISettingsDbService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the settings.
|
||||||
|
/// </summary>
|
||||||
|
Task<FundamentalsSettingsEntity> GetSettingsAsync();
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the settings.
|
||||||
|
/// </summary>
|
||||||
|
Task<FundamentalsSettingsEntity> SaveSettingsAsync(FundamentalsSettingsEntity settings);
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the settings from a dictionary.
|
||||||
|
/// </summary>
|
||||||
|
Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SettingsDbService : ISettingsDbService
|
||||||
|
{
|
||||||
|
private readonly FundamentalsDbContext _context;
|
||||||
|
|
||||||
|
public SettingsDbService(FundamentalsDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the settings asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<FundamentalsSettingsEntity> GetSettingsAsync()
|
||||||
|
{
|
||||||
|
var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync();
|
||||||
|
if (settings == null)
|
||||||
|
{
|
||||||
|
settings = new FundamentalsSettingsEntity { Id = Guid.NewGuid() };
|
||||||
|
_context.Settings.Add(settings);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_context.ChangeTracker.Clear();
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the settings asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<FundamentalsSettingsEntity> SaveSettingsAsync(FundamentalsSettingsEntity settings)
|
||||||
|
{
|
||||||
|
var existing = await _context.Settings.FirstOrDefaultAsync();
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
if (settings.Id == Guid.Empty) settings.Id = Guid.NewGuid();
|
||||||
|
_context.Settings.Add(settings);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.CacheTtlHours = settings.CacheTtlHours;
|
||||||
|
existing.EnableYahooFallback = settings.EnableYahooFallback;
|
||||||
|
existing.UpdatedAt = settings.UpdatedAt;
|
||||||
|
_context.Settings.Update(existing);
|
||||||
|
}
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the settings from a dictionary asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
public async Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary)
|
||||||
|
{
|
||||||
|
var settings = await GetSettingsAsync();
|
||||||
|
|
||||||
|
foreach (var (key, value) in dictionary)
|
||||||
|
{
|
||||||
|
if (string.Equals(key, "CacheTtlHours", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var ttl))
|
||||||
|
settings.CacheTtlHours = ttl;
|
||||||
|
else if (string.Equals(key, "EnableYahooFallback", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var fallback))
|
||||||
|
settings.EnableYahooFallback = fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await SaveSettingsAsync(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,491 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FinlyticCore.Dtos.Yahoo;
|
||||||
|
using FinlyticCore.Services.Yahoo;
|
||||||
|
using FinlyticFundamentals.Entities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Services;
|
||||||
|
|
||||||
|
public interface IYahooFinanceScraper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves ticker from ISIN.
|
||||||
|
/// </summary>
|
||||||
|
Task<string?> ResolveTickerFromIsinAsync(string isin, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves all tickers from ISIN.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<string>> ResolveAllTickersFromIsinAsync(string isin, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scrapes fundamentals.
|
||||||
|
/// </summary>
|
||||||
|
Task<ScrapedFundamentalsData?> ScrapeFundamentalsAsync(string isin, string ticker,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ScrapedFundamentalsData(
|
||||||
|
AssetFundamentalsEntity Fundamentals,
|
||||||
|
TickerFundamentalsEntity TickerData,
|
||||||
|
List<CompanyExecutiveEntity> Executives,
|
||||||
|
List<FinancialStatementEntity> Statements,
|
||||||
|
List<ForwardEstimateEntity> Estimates
|
||||||
|
);
|
||||||
|
|
||||||
|
public class YahooFinanceScraper : IYahooFinanceScraper
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly YahooFinanceClient _yahooClient;
|
||||||
|
private readonly ILogger<YahooFinanceScraper> _logger;
|
||||||
|
|
||||||
|
public YahooFinanceScraper(HttpClient httpClient, YahooFinanceClient yahooClient,
|
||||||
|
ILogger<YahooFinanceScraper> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_yahooClient = yahooClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<string?> ResolveTickerFromIsinAsync(string isin, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tickers = await ResolveAllTickersFromIsinAsync(isin, cancellationToken);
|
||||||
|
return tickers.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<string>> ResolveAllTickersFromIsinAsync(string isin,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(isin)) return new();
|
||||||
|
|
||||||
|
var symbols = new List<(string symbol, int priority)>();
|
||||||
|
|
||||||
|
var primary = await _yahooClient.SearchAsync(isin, quotesCount: 20, cancellationToken: cancellationToken);
|
||||||
|
var quotes = primary?.Quotes ?? new();
|
||||||
|
|
||||||
|
foreach (var q in quotes.Where(q => !string.IsNullOrEmpty(q.Symbol)))
|
||||||
|
{
|
||||||
|
symbols.Add((q.Symbol, GetExchangePriority(q.Symbol, isin)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quotes.Count == 0) return [];
|
||||||
|
|
||||||
|
// 2. Namenssuche für deutsche/andere Handelsplätze
|
||||||
|
var companyName = quotes[0].LongName!;
|
||||||
|
|
||||||
|
var secondary =
|
||||||
|
await _yahooClient.SearchAsync(companyName, quotesCount: 20, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
foreach (var q in secondary?.Quotes ?? new())
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(q.Symbol) &&
|
||||||
|
!symbols.Any(s => s.symbol.Equals(q.Symbol, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
symbols.Add((q.Symbol, GetExchangePriority(q.Symbol, isin)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 3. Sortieren und zurückgeben
|
||||||
|
return symbols
|
||||||
|
.OrderBy(s => s.priority)
|
||||||
|
.Select(s => s.symbol)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Take(20)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetExchangePriority(string symbol, string isin)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(isin) && isin.StartsWith("US", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (!symbol.Contains('.')) return 1;
|
||||||
|
if (symbol.EndsWith(".DE", StringComparison.OrdinalIgnoreCase)) return 2;
|
||||||
|
if (symbol.EndsWith(".F", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
symbol.EndsWith(".SG", StringComparison.OrdinalIgnoreCase)) return 3;
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (symbol.EndsWith(".DE", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 1; // XETRA
|
||||||
|
}
|
||||||
|
else if (symbol.EndsWith(".F", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 2; // Frankfurt
|
||||||
|
}
|
||||||
|
else if (symbol.EndsWith(".TG", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 3; // Gettex
|
||||||
|
}
|
||||||
|
else if (symbol.EndsWith(".MU", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
symbol.EndsWith(".SG", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
symbol.EndsWith(".BE", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
symbol.EndsWith(".DU", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
symbol.EndsWith(".HM", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 4; // Other German regional exchanges
|
||||||
|
}
|
||||||
|
else if (symbol.Contains('.') && !symbol.EndsWith(".OB", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!symbol.EndsWith(".PK", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 5; // Domestic/home non-US exchanges
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 6; // Other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ScrapedFundamentalsData?> ScrapeFundamentalsAsync(string isin, string ticker,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"[{Channel}] Fetching fundamental data for Ticker {Ticker} (ISIN: {Isin}) using YahooFinanceClient...",
|
||||||
|
"FundamentalsChannel", ticker, isin);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var summaryResponse = await _yahooClient.GetFullQuoteSummaryAsync(ticker, cancellationToken);
|
||||||
|
if (summaryResponse?.QuoteSummary?.Result == null || summaryResponse.QuoteSummary.Result.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[{Channel}] YahooFinanceClient returned no result for ticker {Ticker}",
|
||||||
|
"FundamentalsChannel", ticker);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = summaryResponse.QuoteSummary.Result[0];
|
||||||
|
|
||||||
|
var assetProfile = root.AssetProfile;
|
||||||
|
var financialData = root.FinancialData;
|
||||||
|
var defaultKeyStatistics = root.DefaultKeyStatistics;
|
||||||
|
var summaryDetail = root.SummaryDetail;
|
||||||
|
var calendarEvents = root.CalendarEvents;
|
||||||
|
|
||||||
|
// Instantiate entities
|
||||||
|
var fundamentals = new AssetFundamentalsEntity
|
||||||
|
{
|
||||||
|
Isin = isin,
|
||||||
|
PrimaryTicker = ticker,
|
||||||
|
LastUpdatedAt = DateTime.UtcNow,
|
||||||
|
LastStaticUpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
var tickerData = new TickerFundamentalsEntity
|
||||||
|
{
|
||||||
|
Ticker = ticker,
|
||||||
|
Isin = isin,
|
||||||
|
LastUpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Static Profile Data
|
||||||
|
if (assetProfile != null)
|
||||||
|
{
|
||||||
|
fundamentals.BusinessSummary = assetProfile.LongBusinessSummary;
|
||||||
|
fundamentals.Sector = assetProfile.Sector;
|
||||||
|
fundamentals.Industry = assetProfile.Industry;
|
||||||
|
fundamentals.Country = assetProfile.Country;
|
||||||
|
fundamentals.Employees = assetProfile.FullTimeEmployees;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Company Name
|
||||||
|
fundamentals.CompanyName = ticker;
|
||||||
|
|
||||||
|
// 2. Exchange & Trading Currency for Ticker
|
||||||
|
if (financialData != null && !string.IsNullOrWhiteSpace(financialData.FinancialCurrency))
|
||||||
|
{
|
||||||
|
tickerData.TradingCurrency = financialData.FinancialCurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryDetail != null && !string.IsNullOrWhiteSpace(summaryDetail.Currency))
|
||||||
|
{
|
||||||
|
tickerData.TradingCurrency = summaryDetail.Currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Dynamic Price & Valuation Data
|
||||||
|
if (financialData != null)
|
||||||
|
{
|
||||||
|
tickerData.CurrentPrice = financialData.CurrentPrice?.DecimalValue ?? 0;
|
||||||
|
tickerData.GrossMargin = financialData.GrossMargins?.DecimalValue;
|
||||||
|
tickerData.OperatingMargin = financialData.OperatingMargins?.DecimalValue;
|
||||||
|
tickerData.NetProfitMargin = financialData.ProfitMargins?.DecimalValue;
|
||||||
|
tickerData.ReturnOnEquity = financialData.ReturnOnEquity?.DecimalValue;
|
||||||
|
tickerData.ReturnOnAssets = financialData.ReturnOnAssets?.DecimalValue;
|
||||||
|
tickerData.CurrentRatio = financialData.CurrentRatio?.DecimalValue;
|
||||||
|
tickerData.QuickRatio = financialData.QuickRatio?.DecimalValue;
|
||||||
|
tickerData.DebtToEquity = financialData.DebtToEquity?.DecimalValue;
|
||||||
|
|
||||||
|
// Targets on Company Level
|
||||||
|
fundamentals.PriceTargetLow = financialData.TargetLowPrice?.DecimalValue;
|
||||||
|
fundamentals.PriceTargetHigh = financialData.TargetHighPrice?.DecimalValue;
|
||||||
|
fundamentals.PriceTargetMedian = financialData.TargetMedianPrice?.DecimalValue;
|
||||||
|
fundamentals.PriceTargetMean = financialData.TargetMeanPrice?.DecimalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryDetail != null)
|
||||||
|
{
|
||||||
|
if (tickerData.CurrentPrice == 0)
|
||||||
|
{
|
||||||
|
tickerData.CurrentPrice = summaryDetail.Open?.DecimalValue ??
|
||||||
|
summaryDetail.PreviousClose?.DecimalValue ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tickerData.FiftyTwoWeekHigh = summaryDetail.FiftyTwoWeekHigh?.DecimalValue ?? 0;
|
||||||
|
tickerData.FiftyTwoWeekLow = summaryDetail.FiftyTwoWeekLow?.DecimalValue ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mCap = defaultKeyStatistics?.SharesOutstanding?.DecimalValue;
|
||||||
|
mCap ??= summaryDetail?.MarketCap?.DecimalValue;
|
||||||
|
tickerData.MarketCapitalization = mCap ?? 0;
|
||||||
|
|
||||||
|
var ev = defaultKeyStatistics?.EnterpriseValue?.DecimalValue;
|
||||||
|
tickerData.EnterpriseValue = ev ?? 0;
|
||||||
|
|
||||||
|
tickerData.PeRatioTrailing = defaultKeyStatistics?.TrailingEps?.DecimalValue ??
|
||||||
|
summaryDetail?.TrailingPE?.DecimalValue;
|
||||||
|
tickerData.PeRatioForward =
|
||||||
|
defaultKeyStatistics?.ForwardPE?.DecimalValue ?? summaryDetail?.ForwardPE?.DecimalValue;
|
||||||
|
|
||||||
|
if (defaultKeyStatistics != null)
|
||||||
|
{
|
||||||
|
tickerData.PegRatio = defaultKeyStatistics.PegRatio?.DecimalValue;
|
||||||
|
tickerData.PbRatio = defaultKeyStatistics.PriceToBook?.DecimalValue;
|
||||||
|
fundamentals.ShortRatio = defaultKeyStatistics.ShortRatio?.DecimalValue;
|
||||||
|
fundamentals.ShortPercentOfFloat = defaultKeyStatistics.ShortPercentOfFloat?.DecimalValue;
|
||||||
|
fundamentals.PercentHeldByInstitutions = defaultKeyStatistics.HeldPercentInstitutions?.DecimalValue;
|
||||||
|
fundamentals.PercentHeldByInsiders = defaultKeyStatistics.HeldPercentInsiders?.DecimalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tickerData.PsRatio = defaultKeyStatistics?.PriceToSalesTrailing12Months?.DecimalValue ??
|
||||||
|
summaryDetail?.PriceToSalesTrailing12Months?.DecimalValue;
|
||||||
|
tickerData.EvToEbitda = defaultKeyStatistics?.EnterpriseToEbitda?.DecimalValue;
|
||||||
|
tickerData.EvToRevenue = defaultKeyStatistics?.EnterpriseToRevenue?.DecimalValue;
|
||||||
|
|
||||||
|
tickerData.DividendYield = summaryDetail?.DividendYield?.DecimalValue;
|
||||||
|
tickerData.PayoutRatio = summaryDetail?.PayoutRatio?.DecimalValue;
|
||||||
|
|
||||||
|
if (financialData != null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(financialData.RecommendationKey) &&
|
||||||
|
!financialData.RecommendationKey.Equals("none", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
fundamentals.ConsensusRating = financialData.RecommendationKey;
|
||||||
|
}
|
||||||
|
else if (financialData.RecommendationMean != null && financialData.RecommendationMean.Raw.HasValue)
|
||||||
|
{
|
||||||
|
double mean = financialData.RecommendationMean.Raw.Value;
|
||||||
|
fundamentals.ConsensusRating = mean <= 1.8
|
||||||
|
? "strong_buy"
|
||||||
|
: (mean <= 2.5 ? "buy" : (mean <= 3.5 ? "hold" : (mean <= 4.2 ? "sell" : "strong_sell")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Calendar Events Data
|
||||||
|
if (calendarEvents != null)
|
||||||
|
{
|
||||||
|
if (calendarEvents.ExDividendDate?.Raw.HasValue == true)
|
||||||
|
{
|
||||||
|
long seconds = (long)calendarEvents.ExDividendDate.Raw.Value;
|
||||||
|
if (seconds > 0)
|
||||||
|
fundamentals.ExDividendDate = DateTimeOffset.FromUnixTimeSeconds(seconds).UtcDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarEvents.Earnings?.EarningsDate != null && calendarEvents.Earnings.EarningsDate.Count > 0)
|
||||||
|
{
|
||||||
|
var firstDate = calendarEvents.Earnings.EarningsDate[0];
|
||||||
|
if (firstDate.Raw.HasValue && firstDate.Raw.Value > 0)
|
||||||
|
{
|
||||||
|
fundamentals.NextEarningsDate =
|
||||||
|
DateTimeOffset.FromUnixTimeSeconds((long)firstDate.Raw.Value).UtcDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fundamentals.ExDividendDate.HasValue && summaryDetail?.ExDividendDate?.Raw.HasValue == true)
|
||||||
|
{
|
||||||
|
long seconds = (long)summaryDetail.ExDividendDate.Raw.Value;
|
||||||
|
if (seconds > 0) fundamentals.ExDividendDate = DateTimeOffset.FromUnixTimeSeconds(seconds).UtcDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
tickerData.ExDividendDate = fundamentals.ExDividendDate;
|
||||||
|
|
||||||
|
// 5. Executives List
|
||||||
|
var executives = new List<CompanyExecutiveEntity>();
|
||||||
|
if (assetProfile?.CompanyOfficers != null)
|
||||||
|
{
|
||||||
|
foreach (var officer in assetProfile.CompanyOfficers)
|
||||||
|
{
|
||||||
|
var exec = new CompanyExecutiveEntity
|
||||||
|
{
|
||||||
|
Isin = isin,
|
||||||
|
Name = !string.IsNullOrWhiteSpace(officer.Name) ? officer.Name : "Unknown",
|
||||||
|
Title = !string.IsNullOrWhiteSpace(officer.Title) ? officer.Title : "Officer",
|
||||||
|
Age = officer.Age,
|
||||||
|
Compensation = officer.TotalPay?.DecimalValue
|
||||||
|
};
|
||||||
|
executives.Add(exec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Financial Statements
|
||||||
|
var statements = new List<FinancialStatementEntity>();
|
||||||
|
|
||||||
|
// A. Annual Statements
|
||||||
|
if (root.IncomeStatementHistory?.IncomeStatementHistory != null)
|
||||||
|
{
|
||||||
|
foreach (var item in root.IncomeStatementHistory.IncomeStatementHistory)
|
||||||
|
{
|
||||||
|
MapIncomeStatement(item, isin, "Annual", statements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.BalanceSheetHistory?.BalanceSheetStatements != null)
|
||||||
|
{
|
||||||
|
foreach (var item in root.BalanceSheetHistory.BalanceSheetStatements)
|
||||||
|
{
|
||||||
|
MapBalanceSheet(item, isin, "Annual", statements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.CashflowStatementHistory?.CashflowStatements != null)
|
||||||
|
{
|
||||||
|
foreach (var item in root.CashflowStatementHistory.CashflowStatements)
|
||||||
|
{
|
||||||
|
MapCashflowStatement(item, isin, "Annual", statements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// B. Quarterly Statements
|
||||||
|
if (root.IncomeStatementHistoryQuarterly?.IncomeStatementHistory != null)
|
||||||
|
{
|
||||||
|
foreach (var item in root.IncomeStatementHistoryQuarterly.IncomeStatementHistory)
|
||||||
|
{
|
||||||
|
MapIncomeStatement(item, isin, "Quarterly", statements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.BalanceSheetHistoryQuarterly?.BalanceSheetStatements != null)
|
||||||
|
{
|
||||||
|
foreach (var item in root.BalanceSheetHistoryQuarterly.BalanceSheetStatements)
|
||||||
|
{
|
||||||
|
MapBalanceSheet(item, isin, "Quarterly", statements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.CashflowStatementHistoryQuarterly?.CashflowStatements != null)
|
||||||
|
{
|
||||||
|
foreach (var item in root.CashflowStatementHistoryQuarterly.CashflowStatements)
|
||||||
|
{
|
||||||
|
MapCashflowStatement(item, isin, "Quarterly", statements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Forward Estimates
|
||||||
|
var estimates = new List<ForwardEstimateEntity>();
|
||||||
|
|
||||||
|
return new ScrapedFundamentalsData(fundamentals, tickerData, executives, statements, estimates);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{Channel}] Failed to scrape fundamentals for ISIN {Isin} (Ticker: {Ticker})",
|
||||||
|
"FundamentalsChannel", isin, ticker);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MapIncomeStatement(YahooIncomeStatementDto item, string isin, string periodType,
|
||||||
|
List<FinancialStatementEntity> statements)
|
||||||
|
{
|
||||||
|
if (item.EndDate?.Raw.HasValue != true) return;
|
||||||
|
var endDate = DateTimeOffset.FromUnixTimeSeconds((long)item.EndDate.Raw.Value).UtcDateTime.Date;
|
||||||
|
|
||||||
|
var statement = GetOrCreateStatement(statements, isin, periodType, endDate);
|
||||||
|
|
||||||
|
if (item.TotalRevenue?.Raw.HasValue == true) statement.TotalRevenue = item.TotalRevenue.DecimalValue;
|
||||||
|
if (item.CostOfRevenue?.Raw.HasValue == true) statement.CostOfRevenue = item.CostOfRevenue.DecimalValue;
|
||||||
|
if (item.GrossProfit?.Raw.HasValue == true) statement.GrossProfit = item.GrossProfit.DecimalValue;
|
||||||
|
else if (statement.TotalRevenue.HasValue && statement.CostOfRevenue.HasValue)
|
||||||
|
statement.GrossProfit = statement.TotalRevenue - statement.CostOfRevenue;
|
||||||
|
|
||||||
|
if (item.TotalOperatingExpenses?.Raw.HasValue == true)
|
||||||
|
statement.OperatingExpenses = item.TotalOperatingExpenses.DecimalValue;
|
||||||
|
if (item.OperatingIncome?.Raw.HasValue == true) statement.OperatingIncome = item.OperatingIncome.DecimalValue;
|
||||||
|
else if (statement.GrossProfit.HasValue && statement.OperatingExpenses.HasValue)
|
||||||
|
statement.OperatingIncome = statement.GrossProfit - statement.OperatingExpenses;
|
||||||
|
|
||||||
|
if (item.Ebit?.Raw.HasValue == true) statement.Ebitda = item.Ebit.DecimalValue;
|
||||||
|
if (item.NetIncome?.Raw.HasValue == true) statement.NetIncome = item.NetIncome.DecimalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MapBalanceSheet(YahooBalanceSheetStatementDto item, string isin, string periodType,
|
||||||
|
List<FinancialStatementEntity> statements)
|
||||||
|
{
|
||||||
|
if (item.EndDate?.Raw.HasValue != true) return;
|
||||||
|
var endDate = DateTimeOffset.FromUnixTimeSeconds((long)item.EndDate.Raw.Value).UtcDateTime.Date;
|
||||||
|
|
||||||
|
var statement = GetOrCreateStatement(statements, isin, periodType, endDate);
|
||||||
|
|
||||||
|
if (item.Cash?.Raw.HasValue == true) statement.CashAndCashEquivalents = item.Cash.DecimalValue;
|
||||||
|
if (item.NetReceivables?.Raw.HasValue == true) statement.AccountsReceivable = item.NetReceivables.DecimalValue;
|
||||||
|
if (item.Inventory?.Raw.HasValue == true) statement.Inventory = item.Inventory.DecimalValue;
|
||||||
|
if (item.TotalCurrentAssets?.Raw.HasValue == true)
|
||||||
|
statement.TotalCurrentAssets = item.TotalCurrentAssets.DecimalValue;
|
||||||
|
if (item.TotalCurrentLiabilities?.Raw.HasValue == true)
|
||||||
|
statement.CurrentLiabilities = item.TotalCurrentLiabilities.DecimalValue;
|
||||||
|
if (item.LongTermDebt?.Raw.HasValue == true) statement.LongTermDebt = item.LongTermDebt.DecimalValue;
|
||||||
|
if (item.TotalLiab?.Raw.HasValue == true) statement.TotalLiabilities = item.TotalLiab.DecimalValue;
|
||||||
|
if (item.TotalStockholderEquity?.Raw.HasValue == true)
|
||||||
|
statement.TotalStockholdersEquity = item.TotalStockholderEquity.DecimalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MapCashflowStatement(YahooCashflowStatementDto item, string isin, string periodType,
|
||||||
|
List<FinancialStatementEntity> statements)
|
||||||
|
{
|
||||||
|
if (item.EndDate?.Raw.HasValue != true) return;
|
||||||
|
var endDate = DateTimeOffset.FromUnixTimeSeconds((long)item.EndDate.Raw.Value).UtcDateTime.Date;
|
||||||
|
|
||||||
|
var statement = GetOrCreateStatement(statements, isin, periodType, endDate);
|
||||||
|
|
||||||
|
if (item.TotalCashFromOperatingActivities?.Raw.HasValue == true)
|
||||||
|
statement.OperatingCashFlow = item.TotalCashFromOperatingActivities.DecimalValue;
|
||||||
|
if (item.TotalCashflowsFromInvestingActivities?.Raw.HasValue == true)
|
||||||
|
statement.InvestingCashFlow = item.TotalCashflowsFromInvestingActivities.DecimalValue;
|
||||||
|
if (item.CapitalExpenditures?.Raw.HasValue == true)
|
||||||
|
statement.CapitalExpenditures = item.CapitalExpenditures.DecimalValue;
|
||||||
|
if (item.TotalCashFromFinancingActivities?.Raw.HasValue == true)
|
||||||
|
statement.FinancingCashFlow = item.TotalCashFromFinancingActivities.DecimalValue;
|
||||||
|
|
||||||
|
if (statement.OperatingCashFlow.HasValue)
|
||||||
|
{
|
||||||
|
var capex = statement.CapitalExpenditures ?? 0m;
|
||||||
|
statement.FreeCashFlow = statement.OperatingCashFlow.Value - Math.Abs(capex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FinancialStatementEntity GetOrCreateStatement(List<FinancialStatementEntity> statements, string isin,
|
||||||
|
string periodType, DateTime endDate)
|
||||||
|
{
|
||||||
|
var existing = statements.FirstOrDefault(s => s.PeriodType == periodType && s.EndDate.Date == endDate.Date);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
existing = new FinancialStatementEntity
|
||||||
|
{
|
||||||
|
Isin = isin,
|
||||||
|
PeriodType = periodType,
|
||||||
|
EndDate = endDate.Date
|
||||||
|
};
|
||||||
|
statements.Add(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FinlyticCore.Dtos;
|
||||||
|
using FinlyticCore.Models;
|
||||||
|
using FinlyticCore.Util;
|
||||||
|
using FinlyticFundamentals.Services;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Util;
|
||||||
|
|
||||||
|
public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
|
||||||
|
{
|
||||||
|
private readonly ILogger<FundamentalsMqttClient> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IFundamentalsDbService _dbService;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
|
||||||
|
public FundamentalsMqttClient(
|
||||||
|
ILogger<FundamentalsMqttClient> logger,
|
||||||
|
IConfiguration configuration,
|
||||||
|
IFundamentalsDbService dbService,
|
||||||
|
IServiceScopeFactory scopeFactory) : base(logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
_dbService = dbService;
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var config = new MqttConfiguration
|
||||||
|
{
|
||||||
|
Host = _configuration["MQTT:Host"] ?? _configuration["MQTT__Host"] ?? "localhost",
|
||||||
|
Port = Convert.ToInt32(_configuration["MQTT:Port"] ?? _configuration["MQTT__Port"] ?? "1883"),
|
||||||
|
ClientId = _configuration["MQTT:ClientId"] ?? "finlytic_fundamentals_" + Guid.NewGuid().ToString("N")
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] Starting Fundamentals MQTT client. Host: {Host}, ClientId: {ClientId}", "FundamentalsChannel", config.Host, config.ClientId);
|
||||||
|
|
||||||
|
await ConnectAsync(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] Stopping Fundamentals MQTT client.", "FundamentalsChannel");
|
||||||
|
await DisconnectAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] Fundamentals MQTT client connected. Subscribing to RPC request topics...", "FundamentalsChannel");
|
||||||
|
await SubscribeAsync("services/request/fundamentals_Get/#");
|
||||||
|
await SubscribeAsync("services/request/events_GetAll/#");
|
||||||
|
await SubscribeAsync("services/request/health_Ping/#");
|
||||||
|
await SubscribeAsync("services/config/updated/#");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task OnMessageReceivedAsync(string topic, string payload)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(topic)) return;
|
||||||
|
|
||||||
|
// 1. Config update events
|
||||||
|
if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (topic.EndsWith("FinlyticFundamentals", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await OnConfigUpdatedAsync(payload);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract correlationId from topic suffix (e.g. services/request/fundamentals_Get/{correlationId})
|
||||||
|
var lastSlash = topic.LastIndexOf('/');
|
||||||
|
if (lastSlash < 0 || lastSlash >= topic.Length - 1) return;
|
||||||
|
|
||||||
|
var correlationId = topic.Substring(lastSlash + 1);
|
||||||
|
|
||||||
|
// 2. Dispatch to specific channel handlers
|
||||||
|
if (topic.Contains("fundamentals_Get", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await OnFundamentalsGetAsync(payload, correlationId);
|
||||||
|
}
|
||||||
|
else if (topic.Contains("events_GetAll", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await OnEventsGetAllAsync(correlationId);
|
||||||
|
}
|
||||||
|
else if (topic.Contains("health_Ping", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await OnHealthPingAsync(topic, correlationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles fundamentals_Get RPC requests using source-generated DTO deserialization.
|
||||||
|
/// </summary>
|
||||||
|
private async Task OnFundamentalsGetAsync(string payload, string correlationId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payload))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[{Channel}] [FundamentalsMqttClient] Received empty payload for fundamentals_Get request.", "FundamentalsChannel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = (IsinRequest?)JsonSerializer.Deserialize(payload, typeof(IsinRequest), FinlyticJsonSerializerContext.Default);
|
||||||
|
if (request == null || string.IsNullOrWhiteSpace(request.Isin))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[{Channel}] [FundamentalsMqttClient] Request missing mandatory ISIN parameter in payload.", "FundamentalsChannel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] [FundamentalsMqttClient] Processing RPC fundamentals_Get for ISIN '{Isin}' (forceRefresh={ForceRefresh}) [CorrelationId: {CorrelationId}]",
|
||||||
|
"FundamentalsChannel", request.Isin, request.ForceRefresh.ToString(), correlationId);
|
||||||
|
|
||||||
|
var fundamentals = await _dbService.GetFundamentalsAsync(request.Isin, request.Ticker, request.ForceRefresh);
|
||||||
|
var responseTopic = $"services/response/fundamentals_Get/{correlationId}";
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] [FundamentalsMqttClient] Publishing RPC fundamentals response to '{ResponseTopic}'", "FundamentalsChannel", responseTopic);
|
||||||
|
await PublishAsync(responseTopic, fundamentals);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{Channel}] [FundamentalsMqttClient] Failed to process fundamentals_Get request.", "FundamentalsChannel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles events_GetAll RPC requests.
|
||||||
|
/// </summary>
|
||||||
|
private async Task OnEventsGetAllAsync(string correlationId)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] [FundamentalsMqttClient] Processing RPC events_GetAll request [CorrelationId: {CorrelationId}]", "FundamentalsChannel", correlationId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var events = await _dbService.GetAllEventsAsync();
|
||||||
|
var responseTopic = $"services/response/events_GetAll/{correlationId}";
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Channel}] [FundamentalsMqttClient] Publishing events RPC response to '{ResponseTopic}'", "FundamentalsChannel", responseTopic);
|
||||||
|
await PublishAsync(responseTopic, events);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{Channel}] [FundamentalsMqttClient] Failed to process events_GetAll request.", "FundamentalsChannel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles health_Ping RPC requests.
|
||||||
|
/// </summary>
|
||||||
|
private async Task OnHealthPingAsync(string topic, string correlationId)
|
||||||
|
{
|
||||||
|
if (topic.Contains("FinlyticFundamentals", StringComparison.OrdinalIgnoreCase) || !topic.Contains("/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
string respTopic = $"services/response/health_Ping/{correlationId}";
|
||||||
|
await PublishAsync(respTopic, new ServiceHealthResponse("FinlyticFundamentals", "Online", DateTime.UtcNow, "Connected"));
|
||||||
|
_logger.LogInformation("[{Channel}] [FundamentalsMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", "FundamentalsChannel", correlationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles dynamic service config update events.
|
||||||
|
/// </summary>
|
||||||
|
private async Task OnConfigUpdatedAsync(string payload)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Channel}] [FundamentalsMqttClient] Received config update event for FinlyticFundamentals.", "FundamentalsChannel");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(payload);
|
||||||
|
if (doc.RootElement.TryGetProperty("settings", out var settingsProp))
|
||||||
|
{
|
||||||
|
var dict = (Dictionary<string, string>?)JsonSerializer.Deserialize(settingsProp.GetRawText(), typeof(Dictionary<string, string>), FinlyticJsonSerializerContext.Default);
|
||||||
|
if (dict != null && dict.Count > 0)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var settingsDb = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
||||||
|
await settingsDb.UpdateSettingsFromDictionaryAsync(dict);
|
||||||
|
_logger.LogInformation("[{Channel}] [FundamentalsMqttClient] Persisted {Count} updated settings to FinlyticFundamentals database.", "FundamentalsChannel", dict.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{Channel}] [FundamentalsMqttClient] Error processing MQTT config update event.", "FundamentalsChannel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information",
|
||||||
|
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Host=localhost;Database=finlytic_fundamentals;Username=admin;Password=admin"
|
||||||
|
},
|
||||||
|
"MQTT": {
|
||||||
|
"Host": "localhost",
|
||||||
|
"Port": 1883,
|
||||||
|
"ClientId": "finlytic_fundamentals"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
$files = Get-ChildItem -Path "e:\Projects\Finlytic\FinlyticFundamentals" -Recurse -Include *.cs -Exclude "Migrations\*"
|
||||||
|
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$content = Get-Content $file.FullName -Raw
|
||||||
|
|
||||||
|
# regex for logger
|
||||||
|
# match _logger.LogX("message", args)
|
||||||
|
# The tricky part is we need to match the message string correctly.
|
||||||
|
# $content = [regex]::Replace($content, '(_?logger\.Log(?:Information|Warning|Error))\(\s*(ex\s*,\s*)?("[^"]*")\s*(.*?)\)', {
|
||||||
|
# param($match)
|
||||||
|
# ...
|
||||||
|
# })
|
||||||
|
|
||||||
|
# Actually, a simpler way is using a Python script via downloaded Python or just do it in powershell carefully.
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
def process_file(filepath):
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 1. Update logger statements
|
||||||
|
# We want to match: _logger.LogInformation("something", args) or _logger.LogError(ex, "something", args)
|
||||||
|
# This regex is a bit tricky, let's use a simpler approach or careful regex.
|
||||||
|
# Pattern to match logger.Log...( optionally (ex, ) then string then args )
|
||||||
|
|
||||||
|
# Let's match: (logger\.Log[A-Za-z]+)\((.*?)"(.*?)"(.*?)\)
|
||||||
|
# Wait, multi-line strings or strings with escaped quotes might break it.
|
||||||
|
# The regex approach:
|
||||||
|
# Match: (logger\.Log(?:Information|Warning|Error))\(([^"]*)"([^"]*)"(.*)\)
|
||||||
|
# If the string already starts with "[{Channel}] ", skip it.
|
||||||
|
|
||||||
|
def replacer(m):
|
||||||
|
func_part = m.group(1) # e.g. _logger.LogInformation
|
||||||
|
pre_str = m.group(2) # e.g. (ex, or empty if it's the first arg)
|
||||||
|
msg_str = m.group(3)
|
||||||
|
post_str = m.group(4)
|
||||||
|
|
||||||
|
if "[{Channel}]" in msg_str:
|
||||||
|
return m.group(0)
|
||||||
|
|
||||||
|
new_msg = f"[{{Channel}}] {msg_str}"
|
||||||
|
# append "FundamentalsChannel" as the first argument after the string, or right after if there are no args
|
||||||
|
if post_str.strip().startswith(','):
|
||||||
|
# args exist, we need to insert our channel arg before the existing ones, but wait, the channel arg corresponds to {Channel} which is FIRST in the string, so we must pass "FundamentalsChannel" as the FIRST format argument.
|
||||||
|
new_post = f', "FundamentalsChannel"{post_str}'
|
||||||
|
elif post_str.strip() == '':
|
||||||
|
# no extra args, just the closing paren
|
||||||
|
new_post = f', "FundamentalsChannel")'
|
||||||
|
# Note: m.group(4) didn't include the closing paren if we don't match it. Let's adjust the regex to match up to closing paren.
|
||||||
|
else:
|
||||||
|
new_post = f', "FundamentalsChannel"{post_str}'
|
||||||
|
|
||||||
|
return f'{func_part}({pre_str}"{new_msg}"{new_post}'
|
||||||
|
|
||||||
|
# Regex to capture the parts.
|
||||||
|
# group 1: logger.Log...
|
||||||
|
# group 2: anything before the first quote (like exception)
|
||||||
|
# group 3: the string itself
|
||||||
|
# group 4: the rest of the arguments up to the closing parenthesis
|
||||||
|
# We need to find all instances. Let's do a line-by-line or simple regex.
|
||||||
|
|
||||||
|
lines = content.split('\n')
|
||||||
|
new_lines = []
|
||||||
|
|
||||||
|
# 2. Add /// <summary> to public methods
|
||||||
|
# Method pattern: public (async )?(Task|void|[A-Za-z0-9_<>]+) [A-Za-z0-9_]+\(.*\)
|
||||||
|
method_pattern = re.compile(r'^\s*public\s+(?:async\s+)?[A-Za-z0-9_<>\[\]]+\s+[A-Za-z0-9_]+\(.*')
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
# Apply logger transformation
|
||||||
|
# We look for _logger.LogInformation, _logger.LogWarning, _logger.LogError, logger.LogError etc.
|
||||||
|
if '.LogInformation(' in line or '.LogWarning(' in line or '.LogError(' in line:
|
||||||
|
# simple replacement logic
|
||||||
|
match = re.search(r'([_a-zA-Z0-9]+\.Log(?:Information|Warning|Error))\(([^"]*)"(.*?)"(.*)\)', line)
|
||||||
|
if match:
|
||||||
|
func_part = match.group(1)
|
||||||
|
pre_str = match.group(2)
|
||||||
|
msg_str = match.group(3)
|
||||||
|
post_str = match.group(4)
|
||||||
|
|
||||||
|
if "[{Channel}]" not in msg_str:
|
||||||
|
new_msg = f"[{{Channel}}] {msg_str}"
|
||||||
|
if post_str.strip() == ')':
|
||||||
|
new_post = ', "FundamentalsChannel")'
|
||||||
|
elif post_str.endswith(');'):
|
||||||
|
new_post = ', "FundamentalsChannel");'
|
||||||
|
# strip the ); from post_str for clean insertion
|
||||||
|
post_str = post_str[:-2]
|
||||||
|
new_post = f', "FundamentalsChannel"{post_str});'
|
||||||
|
else:
|
||||||
|
new_post = f', "FundamentalsChannel"{post_str}'
|
||||||
|
|
||||||
|
line = f'{line[:match.start()]}{func_part}({pre_str}"{new_msg}"{new_post}{line[match.end():]}'
|
||||||
|
|
||||||
|
# Check for public method to add /// <summary>
|
||||||
|
# We need to make sure we don't add it if it already has one.
|
||||||
|
# Also interface methods: Task<string> Something();
|
||||||
|
if method_pattern.match(line):
|
||||||
|
# Check previous line
|
||||||
|
if i > 0 and '///' not in lines[i-1] and '[' not in lines[i-1]:
|
||||||
|
indent = len(line) - len(line.lstrip())
|
||||||
|
summary = ' ' * indent + '/// <summary>\n' + ' ' * indent + '/// \n' + ' ' * indent + '/// </summary>'
|
||||||
|
new_lines.append(summary)
|
||||||
|
|
||||||
|
# Interface methods inside public interface
|
||||||
|
if re.match(r'^\s*(?:Task|void|[A-Za-z0-9_<>\[\]]+)\s+[A-Za-z0-9_]+\(.*', line):
|
||||||
|
if i > 0 and '///' not in lines[i-1] and '[' not in lines[i-1]:
|
||||||
|
# check if we are in an interface
|
||||||
|
# kinda hard with just line by line, but let's try.
|
||||||
|
pass
|
||||||
|
|
||||||
|
new_lines.append(line)
|
||||||
|
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
f.write('\n'.join(new_lines))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dirs = ['Services', 'Util', '.']
|
||||||
|
base = r'e:\Projects\Finlytic\FinlyticFundamentals'
|
||||||
|
|
||||||
|
for d in dirs:
|
||||||
|
p = os.path.join(base, d)
|
||||||
|
if os.path.isdir(p):
|
||||||
|
for file in os.listdir(p):
|
||||||
|
if file.endswith('.cs'):
|
||||||
|
filepath = os.path.join(p, file)
|
||||||
|
process_file(filepath)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user