Dbmaxwell's picture
feat: deploy FinReport Agent with LightOnOCR review
9703837 verified
Raw
History Blame Contribute Delete
6.37 kB
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,
)