Files
claude-skills-reference/finance/saas-metrics-coach/scripts/metrics_calculator.py
Abbas Mir 176afc5c46 feat(finance): add saas-metrics-coach skill
- SaaS metrics calculator (ARR, MRR, churn, CAC, LTV, NRR)
- Quick Ratio calculator for growth efficiency
- Unit economics simulator for 12-month projections
- Industry benchmarks by stage/segment (OpenView, Bessemer, SaaS Capital)
- 3 stdlib-only Python tools with CLI and JSON output
- Complements financial-analyst skill for SaaS founders
2026-03-08 15:29:03 +00:00

218 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""
SaaS Metrics Calculator — zero external dependencies (stdlib only).
Usage (interactive): python metrics_calculator.py
Usage (CLI): python metrics_calculator.py --mrr 48000 --customers 160 --json
Usage (import):
from metrics_calculator import calculate, report
results = calculate(mrr=48000, mrr_last=42000, customers=160,
churned=4, new_customers=22, sm_spend=18000,
gross_margin=0.72)
print(report(results))
"""
import json
import sys
def calculate(
mrr=None,
mrr_last=None,
customers=None,
churned=None,
new_customers=None,
sm_spend=None,
gross_margin=0.70,
expansion_mrr=0,
churned_mrr=0,
contraction_mrr=0,
profit_margin=None,
):
r, missing = {}, []
# ── Core revenue ─────────────────────────────────────────────────────────
if mrr is not None:
r["MRR"] = round(mrr, 2)
r["ARR"] = round(mrr * 12, 2)
else:
missing.append("ARR/MRR — need current MRR")
if mrr and customers:
r["ARPA"] = round(mrr / customers, 2)
else:
missing.append("ARPA — need MRR + customer count")
# ── Growth ────────────────────────────────────────────────────────────────
if mrr and mrr_last and mrr_last > 0:
r["MoM_Growth_Pct"] = round(((mrr - mrr_last) / mrr_last) * 100, 2)
else:
missing.append("MoM Growth — need last month MRR")
# ── Churn ─────────────────────────────────────────────────────────────────
if churned is not None and customers:
r["Churn_Pct"] = round((churned / customers) * 100, 2)
else:
missing.append("Churn Rate — need churned + total customers")
# ── CAC ───────────────────────────────────────────────────────────────────
if sm_spend and new_customers and new_customers > 0:
r["CAC"] = round(sm_spend / new_customers, 2)
else:
missing.append("CAC — need S&M spend + new customers")
# ── LTV ───────────────────────────────────────────────────────────────────
arpa = r.get("ARPA")
churn_dec = r.get("Churn_Pct", 0) / 100
if arpa and churn_dec > 0:
r["LTV"] = round((arpa / churn_dec) * gross_margin, 2)
else:
missing.append("LTV — need ARPA and churn rate")
# ── LTV:CAC ───────────────────────────────────────────────────────────────
if r.get("LTV") and r.get("CAC") and r["CAC"] > 0:
r["LTV_CAC"] = round(r["LTV"] / r["CAC"], 2)
else:
missing.append("LTV:CAC — need both LTV and CAC")
# ── Payback ───────────────────────────────────────────────────────────────
if r.get("CAC") and arpa and arpa > 0:
r["Payback_Months"] = round(r["CAC"] / (arpa * gross_margin), 1)
else:
missing.append("Payback Period — need CAC and ARPA")
# ── NRR ───────────────────────────────────────────────────────────────────
if mrr_last and mrr_last > 0 and (expansion_mrr or churned_mrr or contraction_mrr):
nrr = ((mrr_last + expansion_mrr - churned_mrr - contraction_mrr) / mrr_last) * 100
r["NRR_Pct"] = round(nrr, 2)
elif r.get("Churn_Pct"):
r["NRR_Est_Pct"] = round((1 - r["Churn_Pct"] / 100) * 100, 2)
missing.append("NRR (accurate) — using churn-only estimate; provide expansion MRR for full NRR")
# ── Rule of 40 ────────────────────────────────────────────────────────────
if r.get("MoM_Growth_Pct") and profit_margin is not None:
r["Rule_of_40"] = round(r["MoM_Growth_Pct"] * 12 + profit_margin, 1)
r["_missing"] = missing
r["_gross_margin"] = gross_margin
return r
def report(r):
labels = [
("MRR", "Monthly Recurring Revenue", "$"),
("ARR", "Annual Recurring Revenue", "$"),
("ARPA", "Avg Revenue Per Account/mo", "$"),
("MoM_Growth_Pct", "MoM MRR Growth", "%"),
("Churn_Pct", "Monthly Churn Rate", "%"),
("CAC", "Customer Acquisition Cost", "$"),
("LTV", "Customer Lifetime Value", "$"),
("LTV_CAC", "LTV:CAC Ratio", ":1"),
("Payback_Months", "CAC Payback Period", " months"),
("NRR_Pct", "NRR (Net Revenue Retention)", "%"),
("NRR_Est_Pct", "NRR Estimate (churn-only)", "%"),
("Rule_of_40", "Rule of 40 Score", ""),
]
lines = ["=" * 54, " SAAS METRICS CALCULATOR", "=" * 54, ""]
for key, label, unit in labels:
val = r.get(key)
if val is None:
continue
if unit == "$":
fmt = f"${val:,.2f}"
elif unit == "%":
fmt = f"{val}%"
elif unit == ":1":
fmt = f"{val}:1"
else:
fmt = f"{val}{unit}"
lines.append(f" {label:<40} {fmt}")
if r.get("_missing"):
lines += ["", " Missing / estimated:"]
for m in r["_missing"]:
lines.append(f" - {m}")
lines.append("=" * 54)
return "\n".join(lines)
# ── Interactive mode ──────────────────────────────────────────────────────────
def _ask(prompt, required=False):
while True:
v = input(f" {prompt}: ").strip()
if not v:
if required:
print(" Required — please enter a value.")
continue
return None
try:
return float(v)
except ValueError:
print(" Enter a number (e.g. 48000 or 72).")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="SaaS Metrics Calculator")
parser.add_argument("--mrr", type=float, help="Current MRR")
parser.add_argument("--mrr-last", type=float, help="MRR last month")
parser.add_argument("--customers", type=int, help="Total active customers")
parser.add_argument("--churned", type=int, help="Customers churned this month")
parser.add_argument("--new-customers", type=int, help="New customers acquired")
parser.add_argument("--sm-spend", type=float, help="Sales & Marketing spend")
parser.add_argument("--gross-margin", type=float, default=70, help="Gross margin %% (default: 70)")
parser.add_argument("--expansion-mrr", type=float, default=0, help="Expansion MRR")
parser.add_argument("--churned-mrr", type=float, default=0, help="Churned MRR")
parser.add_argument("--contraction-mrr", type=float, default=0, help="Contraction MRR")
parser.add_argument("--profit-margin", type=float, help="Net profit margin %%")
parser.add_argument("--json", action="store_true", help="Output JSON format")
args = parser.parse_args()
# CLI mode
if args.mrr is not None:
inputs = {
"mrr": args.mrr,
"mrr_last": args.mrr_last,
"customers": args.customers,
"churned": args.churned,
"new_customers": args.new_customers,
"sm_spend": args.sm_spend,
"gross_margin": args.gross_margin / 100 if args.gross_margin > 1 else args.gross_margin,
"expansion_mrr": args.expansion_mrr,
"churned_mrr": args.churned_mrr,
"contraction_mrr": args.contraction_mrr,
"profit_margin": args.profit_margin,
}
result = calculate(**inputs)
if args.json:
print(json.dumps(result, indent=2))
else:
print("\n" + report(result))
sys.exit(0)
# Interactive mode
print("\nSaaS Metrics Calculator (press Enter to skip)\n")
gm = _ask("Gross margin % (default 70)", required=False) or 70
inputs = dict(
mrr=_ask("Current MRR ($)", required=True),
mrr_last=_ask("MRR last month ($)"),
customers=_ask("Total active customers"),
churned=_ask("Customers churned this month"),
new_customers=_ask("New customers acquired this month"),
sm_spend=_ask("Sales & Marketing spend this month ($)"),
gross_margin=gm / 100 if gm > 1 else gm,
expansion_mrr=_ask("Expansion MRR (upsells) ($)") or 0,
churned_mrr=_ask("Churned MRR ($)") or 0,
contraction_mrr=_ask("Contraction MRR (downgrades) ($)") or 0,
profit_margin=_ask("Net profit margin % (for Rule of 40, optional)"),
)
print("\n" + report(calculate(**inputs)))