-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaudit_optimizer.py
More file actions
80 lines (63 loc) · 3.68 KB
/
audit_optimizer.py
File metadata and controls
80 lines (63 loc) · 3.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
"""VynFi audit optimizer — risk-scope + portfolio + Monte Carlo (Scale+).
Wraps DS 4.1.2's `datasynth-data optimizer` subcommands. This example:
1. Sketches a small audit engagement payload (accounts + controls).
2. Ranks accounts by residual risk (``optimizer.risk_scope``).
3. Allocates 200 audit hours across candidate procedures
(``optimizer.portfolio``).
4. Runs 5,000 Monte Carlo draws to estimate risk-weighted coverage
(``optimizer.monte_carlo``).
DS 4.1.2 returns stub reports (``status: stub_report_v4_1_2``). The schemas
are stable — real analytics light up in later 4.1.x / 4.2.x releases. The
SDK wraps everything in :class:`OptimizerResponse` with an opaque ``.report``
payload so this example keeps working as the analytics fill in.
"""
from __future__ import annotations
import json
import os
import vynfi
client = vynfi.VynFi(api_key=os.environ["VYNFI_API_KEY"])
# ── 1. Describe a small audit engagement ────────────────────────────────────
engagement = {
"engagementId": "ENG-2026-001",
"entity": "ACME Retail AG",
"fiscalYear": 2025,
"materiality": 500_000,
"accounts": [
{"code": "1000", "name": "Cash", "balance": 2_100_000, "volatility": 0.05},
{"code": "1200", "name": "Accounts Receivable", "balance": 14_800_000, "volatility": 0.22},
{"code": "1400", "name": "Inventory", "balance": 9_300_000, "volatility": 0.31},
{"code": "2000", "name": "Accounts Payable", "balance": 6_700_000, "volatility": 0.18},
{"code": "4000", "name": "Revenue", "balance": 82_500_000, "volatility": 0.27},
{"code": "5000", "name": "Cost of Sales", "balance": 52_100_000, "volatility": 0.24},
],
"controls": [
{"id": "C-AR-01", "coverage": 0.85, "effectiveness": 0.80},
{"id": "C-INV-01", "coverage": 0.60, "effectiveness": 0.55},
{"id": "C-PO-03", "coverage": 0.90, "effectiveness": 0.88},
],
}
# ── 2. Rank accounts by residual risk ────────────────────────────────────────
print("=== optimizer.risk_scope(top_n=3) ===")
rs = client.optimizer.risk_scope(engagement=engagement, top_n=3)
print(json.dumps(rs.report, indent=2, default=str)[:600])
# ── 3. Portfolio allocation across candidate procedures ──────────────────────
candidates = [
{"id": "P-AR-TOE", "account": "1200", "hoursEstimate": 60, "riskScore": 0.82},
{"id": "P-AR-SAM", "account": "1200", "hoursEstimate": 30, "riskScore": 0.55},
{"id": "P-INV-COUNT", "account": "1400", "hoursEstimate": 80, "riskScore": 0.77},
{"id": "P-INV-NRV", "account": "1400", "hoursEstimate": 25, "riskScore": 0.45},
{"id": "P-REV-CUT", "account": "4000", "hoursEstimate": 45, "riskScore": 0.91},
{"id": "P-COS-MATCH", "account": "5000", "hoursEstimate": 40, "riskScore": 0.68},
]
print("\n=== optimizer.portfolio(budget_hours=200) ===")
port = client.optimizer.portfolio(candidates=candidates, budget_hours=200)
print(json.dumps(port.report, indent=2, default=str)[:600])
# ── 4. Monte Carlo risk-weighted coverage ────────────────────────────────────
print("\n=== optimizer.monte_carlo(runs=5000, seed=7) ===")
mc = client.optimizer.monte_carlo(engagement=engagement, runs=5000, seed=7)
print(json.dumps(mc.report, indent=2, default=str)[:600])
print(
"\nTip: pair these with `client.jobs.generate_config(...)` using"
"\n `config.audit.enabled = True` to get a concrete engagement payload"
"\n straight out of DS 4.1 instead of the hand-sketched one above."
)