| from __future__ import annotations |
|
|
| import re |
| from decimal import Decimal |
|
|
| from .models import ( |
| FundPortfolioReport, |
| Severity, |
| ValidationFinding, |
| ValidationResult, |
| ) |
|
|
|
|
| ISIN_PATTERN = re.compile(r"^TR[A-Z0-9]{10}$") |
| CENT = Decimal("0.01") |
|
|
|
|
| def _difference_finding( |
| *, |
| code: str, |
| message: str, |
| expected: Decimal, |
| actual: Decimal, |
| tolerance: Decimal, |
| sheet: str, |
| severity: Severity = Severity.ERROR, |
| record_key: str | None = None, |
| page: int | None = None, |
| ) -> ValidationFinding | None: |
| if abs(expected - actual) <= tolerance: |
| return None |
| return ValidationFinding( |
| severity=severity, |
| code=code, |
| message=message, |
| sheet=sheet, |
| record_key=record_key, |
| page=page, |
| expected=str(expected), |
| actual=str(actual), |
| ) |
|
|
|
|
| def validate_report(report: FundPortfolioReport) -> ValidationResult: |
| findings: list[ValidationFinding] = [] |
|
|
| for row in report.equities: |
| if not ISIN_PATTERN.match(row.isin): |
| findings.append( |
| ValidationFinding( |
| severity=Severity.ERROR, |
| code="INVALID_ISIN", |
| message="ISIN biçimi geçersiz.", |
| sheet="Hisse Senetleri", |
| record_key=row.code, |
| page=row.source.page, |
| actual=row.isin, |
| ) |
| ) |
|
|
| calculated = row.nominal * row.current_price |
| tolerance = max(Decimal("1.00"), abs(row.total_value) * Decimal("0.000001")) |
| finding = _difference_finding( |
| code="POSITION_VALUE_MISMATCH", |
| message="Nominal değer × güncel fiyat, toplam değerle uzlaşmıyor.", |
| expected=row.total_value, |
| actual=calculated, |
| tolerance=tolerance, |
| sheet="Hisse Senetleri", |
| record_key=row.code, |
| page=row.source.page, |
| ) |
| if finding: |
| findings.append(finding) |
|
|
| totals = report.declared_totals |
| calculated_equity_nominal = sum( |
| (row.nominal for row in report.equities), start=Decimal("0") |
| ) |
| calculated_equity_value = sum( |
| (row.total_value for row in report.equities), start=Decimal("0") |
| ) |
| calculated_repo_value = sum( |
| (row.total_value for row in report.repos), start=Decimal("0") |
| ) |
| calculated_derivative = sum( |
| (row.total_value for row in report.derivatives), start=Decimal("0") |
| ) |
| calculated_collateral = sum( |
| (row.total_value for row in report.cash_collaterals), start=Decimal("0") |
| ) |
|
|
| aggregate_checks = [ |
| _difference_finding( |
| code="EQUITY_NOMINAL_ROUNDING", |
| message=( |
| "Detay nominal toplamı ile PDF beyan toplamı arasında fark var. " |
| "Bu fark, kaynakta satırların iki ondalıkla gösterilmesinden kaynaklanabilir." |
| ), |
| expected=totals.equity_nominal, |
| actual=calculated_equity_nominal, |
| tolerance=Decimal("0.00"), |
| sheet="Hisse Senetleri", |
| severity=Severity.WARNING, |
| ), |
| _difference_finding( |
| code="EQUITY_TOTAL_MISMATCH", |
| message="Hisse detay toplamı PDF grup toplamıyla uzlaşmıyor.", |
| expected=totals.equity_value, |
| actual=calculated_equity_value, |
| tolerance=CENT, |
| sheet="Hisse Senetleri", |
| ), |
| _difference_finding( |
| code="REPO_TOTAL_MISMATCH", |
| message="Repo detay toplamı PDF grup toplamıyla uzlaşmıyor.", |
| expected=totals.repo_value, |
| actual=calculated_repo_value, |
| tolerance=CENT, |
| sheet="T. Repo", |
| ), |
| _difference_finding( |
| code="DERIVATIVE_TOTAL_MISMATCH", |
| message="Türev detay toplamı PDF grup toplamıyla uzlaşmıyor.", |
| expected=totals.derivative_exposure, |
| actual=calculated_derivative, |
| tolerance=CENT, |
| sheet="Türev", |
| ), |
| _difference_finding( |
| code="COLLATERAL_TOTAL_MISMATCH", |
| message="Nakit teminat toplamı PDF grup toplamıyla uzlaşmıyor.", |
| expected=totals.cash_collateral_value, |
| actual=calculated_collateral, |
| tolerance=CENT, |
| sheet="Türev", |
| ), |
| ] |
| findings.extend(finding for finding in aggregate_checks if finding) |
|
|
| calculated_portfolio = ( |
| totals.equity_value + totals.repo_value + totals.cash_collateral_value |
| ) |
| portfolio_finding = _difference_finding( |
| code="PORTFOLIO_RECONCILIATION_FAILED", |
| message=( |
| "Hisse + repo + VİOP nakit teminatı, fon portföy değeriyle uzlaşmıyor." |
| ), |
| expected=totals.fund_portfolio_value, |
| actual=calculated_portfolio, |
| tolerance=CENT, |
| sheet="Özet", |
| ) |
| if portfolio_finding: |
| findings.append(portfolio_finding) |
|
|
| fpd_sum = ( |
| totals.equity_fpd_pct |
| + totals.repo_fpd_pct |
| + totals.cash_collateral_fpd_pct |
| ) |
| fpd_finding = _difference_finding( |
| code="FPD_PERCENT_MISMATCH", |
| message="FPD yüzdeleri %100'e ulaşmıyor.", |
| expected=Decimal("1"), |
| actual=fpd_sum, |
| tolerance=Decimal("0.0001"), |
| sheet="Özet", |
| ) |
| if fpd_finding: |
| findings.append(fpd_finding) |
|
|
| findings.append( |
| ValidationFinding( |
| severity=Severity.INFO, |
| code="DERIVATIVE_EXPOSURE_DISCLOSURE", |
| message=( |
| "Futures değeri maruziyet olarak raporlandı; fon portföy değeri " |
| "uzlaştırmasına dahil edilmedi." |
| ), |
| sheet="Özet", |
| expected=str(totals.derivative_exposure), |
| ) |
| ) |
|
|
| error_count = sum(item.severity == Severity.ERROR for item in findings) |
| warning_count = sum(item.severity == Severity.WARNING for item in findings) |
| info_count = sum(item.severity == Severity.INFO for item in findings) |
| status = "Başarısız" if error_count else "Uyarılı" if warning_count else "Doğrulandı" |
| return ValidationResult( |
| status=status, |
| findings=findings, |
| error_count=error_count, |
| warning_count=warning_count, |
| info_count=info_count, |
| ) |
|
|