Custom strategy

  1. Guidelines
  2. Composing strategies

When rules are not enough (a scorecard, an external scoring service, an ML model), implement RiskStrategy directly. It is the same seam the rule-based default and the AI strategy use, so the rest of the system does not change.

use Gawrys\Counterparty\Risk\{RiskStrategy, RiskAssessment, Evidence};
use Gawrys\Counterparty\Enum\RiskLevel;

final class ScorecardStrategy implements RiskStrategy
{
    public function assess(Counterparty $counterparty, VerificationReport $report): RiskAssessment
    {
        // The report is ground truth: never override a hard sanctions FAIL.
        if ($report->hasAdverseFindings()) {
            return new RiskAssessment(RiskLevel::Critical, 1.0, 'Adverse hard fact present.', humanReviewRequired: true);
        }

        $score = $this->scorecard($counterparty, $report); // your model -> 0.0-1.0

        return new RiskAssessment(
            level: RiskLevel::fromScore($score),
            score: $score,
            summary: 'Scorecard result.',
            humanReviewRequired: $score >= 0.6 || $report->hasInconclusive(),
            evidence: [Evidence::grounded('Scorecard v3', 'https://internal/scorecard', 0.8)],
        );
    }
}

Guidelines

  • Respect the hard facts. The report is deterministic ground truth; a custom strategy should never downgrade an adverse finding. Treat the assessment as advice layered on top.
  • Set requiresHumanReview() honestly - on adverse facts, on inconclusive coverage, and when your own confidence is low.
  • Ground your evidence. Prefer Evidence::grounded() with a real source; reserve ungrounded() for low-confidence hints.
  • Keep it deterministic where you can, so results are cacheable and reproducible.

Composing strategies

A strategy can wrap another - e.g. run the rule-based default, then add an external score, or fall back to it when your service is unavailable:

final class HybridStrategy implements RiskStrategy
{
    public function __construct(
        private RiskStrategy $primary,
        private RiskStrategy $fallback,
    ) {}

    public function assess(Counterparty $c, VerificationReport $r): RiskAssessment
    {
        try {
            return $this->primary->assess($c, $r);
        } catch (\Throwable) {
            return $this->fallback->assess($c, $r);
        }
    }
}

The AI strategy is exactly such a drop-in - it consumes the report and returns a RiskAssessment, advisory only.


Counterparty Verification - a due-diligence aid, not a compliance product. MIT licensed.

This site uses Just the Docs, a documentation theme for Jekyll.