Appearance
SYSCOHADA & ratios
100 % deterministic ("Zero LLM Math"). Three implementations agree on the same algebra:
| Engine | Path | Used for |
|---|---|---|
| Python (worker) | locus-workers/table_extractor/parser.py, ratio_engine/calculator.py | Decimal math, ROUND_HALF_UP, regression pack |
| TypeScript | locus-web/src/engine/ratio_calculator.ts (computeSyscohadaRatios) + components/ratio-cockpit/DeterministicRatioCockpit.tsx | client cockpit, copilot tool get_syscohada_ratios |
| C# (SoR) | locus-backend/Services/DossierAnalyticsService.cs | persistence of FinancialStatements / ExtractedCells / RatioSnapshots during analysis |
1. Extract
- Taxonomy line codes (EBE, cash, LTD, STD, …) from PDF text / worker extract.
- Invariants: Actif ≡ Passif (within tolerance); VA − Personnel ≡ EBE style checks in analytics.
- Persistence:
FinancialStatements+ExtractedCells(+ optionalSourceCitationId).
mermaid
flowchart LR
OCR[ExtractedPages text + WordsJson] --> W[POST /api/v1/extract-syscohada]
W -->|"≥ 3 values"| Cells
W -.->|offline or < 3 values| Local[ExtractCellsFromText in-process]
Local --> Cells[cells: line code → value]
Cells --> Cite[MatchLineCitations → Citations]
Cells --> FS[FinancialStatement + ExtractedCells]
Cells --> RS[RatioSnapshot]
FS --> Rec{|Actif − Passif| ≤ tolerance}
Rec -->|yes| OK[RECONCILED]
Rec -->|no| UN[UNRECONCILED → covenantCertificationBlocked]Regex taxonomy (SYSCOHADA_TAXONOMY)
Multi-pattern regex dictionary matching accounting items across standard variations (technical documentation §4.1):
| Canonical key | Statement | Representative patterns |
|---|---|---|
revenue | SIG | chiffre\s+d['’]affaires, ventes\s+de\s+marchandises, produits\s+d['’]exploitation |
value_added | SIG | valeur\s+ajout[eé]e, v\.a\. |
personnel_expenses | SIG | charges\s+de\s+personnel, frais\s+de\s+personnel, salaires\s+et\s+traitements |
ebitda (EBE) | SIG | exc[eé]dent\s+brut\s+d['’]exploitation, e\.b\.e\., ebitda |
depreciation_amortization | SIG | dotations\s+aux\s+amortissements, … |
ebit (REX) | SIG | r[eé]sultat\s+d['’]exploitation, r\.e\.x\., ebit |
financial_income / financial_expenses | SIG | produits\s+financiers / charges\s+financi[eè]res, frais\s+financiers |
net_ordinary_income (RAO), exceptional_income (RHAO), corporate_tax, net_income | SIG | RAO / RHAO / i\.s\. / résultat net |
fixed_assets, inventories, trade_receivables, other_current_assets | Bilan Actif | actif\s+immobilis[eé], stocks, cr[eé]ances\s+clients, autres\s+cr[eé]ances |
cash_and_equivalents (Trésorerie Actif) | Bilan Actif | tr[eé]sorerie\s+actif, banques,\s+ch[eè]ques\s+postaux,\s+caisse, disponibilit[eé]s |
total_assets | Bilan Actif | total\s+actif, total\s+g[eé]n[eé]ral\s+actif |
equity | Bilan Passif | capitaux\s+propres, capital\s+social |
long_term_financial_debt (Dettes Financières LMT) | Bilan Passif | dettes\s+financi[eè]res\s+et\s+assimil[eé]es, emprunts\s+obligataires, emprunts\s+aupr[eè]s\s+des\s+[eé]tablissements\s+de\s+cr[eé]dit |
short_term_financial_debt (Trésorerie Passif) | Bilan Passif | d[eé]couverts\s+et\s+concours\s+bancaires, tr[eé]sorerie\s+passif, emprunts\s+[aà]\s+court\s+terme |
trade_payables, tax_and_social_payables, other_current_payables | Bilan Passif | dettes\s+fournisseurs, dettes\s+fiscales\s+et\s+sociales, autres\s+dettes\s+circulantes |
total_liabilities | Bilan Passif | total\s+passif, total\s+g[eé]n[eé]ral\s+passif |
capex, scheduled_principal_repayment | SIG | acquisitions\s+d'immobilisations, remboursement\s+du\s+principal (SoR taxonomy) |
The SoR variant (DossierAnalyticsService.Taxonomy) does the same with normalized substring matches and takes the last French-formatted number on the matching line; zero values are ignored.
2. Invariants
Two non-negotiable structural equalities (technical documentation §4.1):
[ \Delta_{\text{Balance}} = \lvert \text{Total Actif} - \text{Total Passif} \rvert \equiv 0 ]
[ \Delta_{\text{SIG}} = \lvert (\text{Valeur Ajoutée} - \text{Charges de Personnel}) - \text{EBE} \rvert \equiv 0 ]
If (\Delta > 0) the parser records an accounting discrepancy and flags the line items. A mismatch marks the statement UNRECONCILED and blocks covenant certification. Analyst adjustments capture the prior value, rationale and user id and recalculate dependent ratios.
Tolerances by layer
| Layer | Test | Tolerance |
|---|---|---|
Python compute_period_ratios | is_balance_reconciled, is_ebe_reconciled | |Δ| < 0.01 (currency units) |
TypeScript computeSyscohadaRatios | same | |Δ| < 1.0 |
SoR UpsertFinancialStatementAsync | ReconciliationState | RECONCILED iff Δ ≤ max(1 000 000, 0.5 % × totalAssets) (XAF), else UNRECONCILED |
SoR GET /api/deals/{id}/reconciliation | covenantCertificationBlocked | statements with |Actif − Passif| ≥ 0.01 flagged UNRECONCILED (technical doc §10.16) |
AUDCIF 2017 Art. 17 requires both totals to be equal at each closing date; an unreconciled balance sheet is never accepted as-is (see OCR override SOP).
Worked proof (SEEG 2023, reference fixture)
| Check | Computation | Result |
|---|---|---|
| Total Actif | 142.5 + 18.2 + 42.8 + 9.4 + 14.8 (B FCFA) | 227.70 B |
| Total Passif | 58.4 + 72.5 + 30.2 + 38.6 + 16.8 + 11.2 | 227.70 B |
| (\Delta_{\text{Balance}}) | (\lvert 227.7 - 227.7 \rvert) | 0 FCFA |
| EBE (SIG) | 62.35 (VA) − 27.50 (personnel) | 34.85 B = reported EBE, (\Delta_{\text{SIG}} = 0) |
| Net debt | (72.5 + 30.2) − 14.8 | 87.90 B |
| Levier net | 87.90 / 34.85 | 2.52× (ceiling 3.50×, headroom 0.98×) |
SEEG is a test-reference dataset; fresh tenants never see it.
3. Ratio definitions
Core formulas (technical documentation §4.2; same in Python and TS):
[ \text{Gross Debt} = \text{Dettes Financières LMT} + \text{Dettes CT} \qquad \text{Net Debt} = \text{Gross Debt} - \text{Trésorerie Actif} ]
[ \text{Leverage} = \frac{\text{Net Debt}}{\text{EBE}} ;[\le 3.50\times] \qquad \text{ICR} = \frac{\text{REX (EBIT)}}{\text{Charges Financières}} ;[\ge 2.50\times] ]
[ \text{DSCR} = \frac{\text{CFADS}}{\text{Remboursement Principal} + \text{Intérêts Financiers}} ]
[ \text{FRNG} = (\text{Capitaux Propres} + \text{Dettes LMT}) - \text{Actif Immobilisé} \qquad \text{BFR} = (\text{Stocks} + \text{Créances Clients} + \text{Autres Créances}) - (\text{Fournisseurs} + \text{Fiscal/Social} + \text{Autres Dettes}) ]
[ \text{Trésorerie Nette} = \text{FRNG} - \text{BFR} \qquad \text{BFR}_{\text{jours}} = \frac{\text{BFR}}{\text{Chiffre d'Affaires}} \times 365 ]
| Ratio | Code definition | Guard (zero denominator) |
|---|---|---|
| Net debt / EBE | net_debt / ebitda | ebitda ≤ 0 → 999 |
| Gearing | net_debt / equity | equity ≤ 0 → 0 |
| Debt / capitalization | gross_debt / (gross_debt + equity) | — |
| ICR | ebit / financial_expenses | financial_expenses ≤ 0 → 999 |
| EBITDA ICR | ebitda / financial_expenses | same |
| DSCR | (ebitda − corporate_tax − capex) / (scheduled_principal_repayment + financial_expenses) | debt_service ≤ 0 → null |
| FRNG, BFR, net cash | as above (other_current_payables included in BFR liabilities) | — |
| BFR days | bfr / revenue × 365 (1 decimal) | revenue ≤ 0 → 0 |
| Current / quick ratio | (op. current assets + cash) / (op. current liabilities + STD); quick = (receivables + cash) / … | denominator ≤ 0 → 999 |
| Margins | EBITDA / EBIT / net ÷ revenue × 100 | revenue ≤ 0 → 0 |
CFADS in code vs. narrative
The technical documentation writes CFADS as EBE − Impôts − ΔBFR − CAPEX; the shipped engines (Python, TS, SoR snapshot) currently use EBITDA − corporate tax − CAPEX (no ΔBFR term), and the SoR snapshot uses ebitda / (financial_expenses + principal). Treat narrative DSCR as indicative; the cockpit/SoR value is authoritative.
Rounding
round_ratio quantizes with ROUND_HALF_UP (2 places; BFR days 1 place) to avoid floating-point drift. TS uses Number(x.toFixed(n)). SoR persists Math.Round(x, 4) for leverage / DSCR / ICR and Math.Round(netDebt, 2).
Covenant evaluation matrix
| Covenant | Threshold | Python worker | SoR AnalysisResult |
|---|---|---|---|
| Net Debt / EBITDA | ≤ 3.50× | BREACH (CRITICAL) above; WARNING (MAJOR) within 10 % (> 3.15×); else COMPLIANT | BREACH if > 3.50, else PASS |
| ICR | ≥ 2.50× | BREACH (CRITICAL) below | BREACH if 0 < ICR < 2.50 |
| DSCR | ≥ 1.30× (default min_dscr) | BREACH (CRITICAL) below | limit 1.20, WARNING if 0 < DSCR < 1.20 |
| Current ratio | ≥ 1.10× (threshold defined) | — | — |
Technical documentation narrative: DSCR < 1.30× → WARNING / MAJOR; the Credit Scenario Lab raises COVENANT_BREACH_ALERT when stressed DSCR < 1.20×. Some narrative screens use story floors of 1.20× versus the cockpit default 1.30× (see UI tours). Boundary tests: BREACH at 3.51×, COMPLIANT at 3.50× (tests/test_ratio_engine.py).
RatioSnapshot persistence
ratio_{dossierId}_{year}_{scenario} (lower-case), FormulaId = STD_SYSCOHADA_V2, NetDebtAmount, LeverageRatio, DscrValue, IcrValue, CovenantBreached = (leverage > 3.5) || (ICR < 2.5) || (DSCR < 1.2) — updated on every analysis run.
4. Real-time sensitivity stress-tester (cockpit)
In the frontend cockpit sliders recalculate with no network round-trip:
| Slider | Range | Effect |
|---|---|---|
| Choc Chiffre d'Affaires (\Delta\text{CA}) | −25 % … +25 % | Adjusts revenue; EBITDA moves (1.5\times) operating leverage, EBIT (1.8\times) |
| Hausse des Taux / Marge | 0 … +400 bps | Expands interest expense → ICR degradation |
| Surcoût CAPEX | 0 … +50 % | DSCR compression under construction / maintenance overruns |
Copilot tools simulate_credit_stress (EBITDA shocks −10/−20/−30 %) and audit_covenants reuse these engines.
5. Override
POST /api/locus/deals/{id}/cells/{cellId}/override (SoR POST /api/deals/{id}/cells/{cellId}/override, permission financial:override) — keeps ReportedValue, sets accepted value + reason (min 5 characters) + audit CELL_OVERRIDE; the BFF validates with CellOverrideSchema and re-computes dependent ratios through reconciliationService.adjustCell. Full procedure and request body: OCR override SOP.
json
{ "acceptedValue": 1245600, "reason": "p.14 Total Passif misread by OCR; confirmed on stamped page" }6. Tests
| File | Cases |
|---|---|
tests/test_table_extractor.py | test_taxonomy_mapping, test_balance_sheet_invariants, test_sig_invariants |
tests/test_ratio_engine.py | test_ratio_computation_accuracy, test_covenant_evaluator, test_rounding_and_precision |
locus-web/tests/reconciliation_sor_bff.test.ts, reference_regression_pack.test.ts, sor_dossier_analytics_unit.test.ts | SoR reconciliation, SEEG regression lock (2.52× vs 3.50×) |
Related: Citations (R4) · Contradictions · Dossier → OCR → analysis.