Merge pull request #298 from abbasmir12/feat/finance-saas-metrics-coach
feat(finance): add saas-metrics-coach skill
This commit is contained in:
@@ -100,3 +100,6 @@ python financial-analyst/scripts/forecast_builder.py forecast_data.json --format
|
||||
**Last Updated:** February 2026
|
||||
**Skills Deployed:** 1/1 finance skills production-ready
|
||||
**Total Tools:** 4 Python automation tools
|
||||
|
||||
## saas-metrics-coach
|
||||
SaaS financial health advisor. Calculates ARR, MRR, churn, CAC, LTV, NRR, Quick Ratio. Benchmarks against industry standards. Includes 12-month projection simulator.
|
||||
|
||||
158
finance/saas-metrics-coach/SKILL.md
Normal file
158
finance/saas-metrics-coach/SKILL.md
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
name: saas-metrics-coach
|
||||
description: SaaS financial health advisor. Use when a user shares revenue or customer numbers, or mentions ARR, MRR, churn, LTV, CAC, NRR, or asks how their SaaS business is doing.
|
||||
license: MIT
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
author: Abbas Mir
|
||||
category: finance
|
||||
updated: 2026-03-08
|
||||
---
|
||||
|
||||
# SaaS Metrics Coach
|
||||
|
||||
Act as a senior SaaS CFO advisor. Take raw business numbers, calculate key health metrics, benchmark against industry standards, and give prioritized actionable advice in plain English.
|
||||
|
||||
## Step 1 — Collect Inputs
|
||||
|
||||
If not already provided, ask for these in a single grouped request:
|
||||
|
||||
- Revenue: current MRR, MRR last month, expansion MRR, churned MRR
|
||||
- Customers: total active, new this month, churned this month
|
||||
- Costs: sales and marketing spend, gross margin %
|
||||
|
||||
Work with partial data. Be explicit about what is missing and what assumptions are being made.
|
||||
|
||||
## Step 2 — Calculate Metrics
|
||||
|
||||
Run `scripts/metrics_calculator.py` with the user's inputs. If the script is unavailable, use the formulas in `references/formulas.md`.
|
||||
|
||||
Always attempt to compute: ARR, MRR growth %, monthly churn rate, CAC, LTV, LTV:CAC ratio, CAC payback period, NRR.
|
||||
|
||||
**Additional Analysis Tools:**
|
||||
- Use `scripts/quick_ratio_calculator.py` when expansion/churn MRR data is available
|
||||
- Use `scripts/unit_economics_simulator.py` for forward-looking projections
|
||||
|
||||
## Step 3 — Benchmark Each Metric
|
||||
|
||||
Load `references/benchmarks.md`. For each metric show:
|
||||
- The calculated value
|
||||
- The relevant benchmark range for the user's segment and stage
|
||||
- A plain status label: HEALTHY / WATCH / CRITICAL
|
||||
|
||||
Match the benchmark tier to the user's market segment (Enterprise / Mid-Market / SMB / PLG) and company stage (Early / Growth / Scale). Ask if unclear.
|
||||
|
||||
## Step 4 — Prioritize and Recommend
|
||||
|
||||
Identify the top 2-3 metrics at WATCH or CRITICAL status. For each one state:
|
||||
- What is happening (one sentence, plain English)
|
||||
- Why it matters to the business
|
||||
- Two or three specific actions to take this month
|
||||
|
||||
Order by impact — address the most damaging problem first.
|
||||
|
||||
## Step 5 — Output Format
|
||||
|
||||
Always use this exact structure:
|
||||
|
||||
```
|
||||
# SaaS Health Report — [Month Year]
|
||||
|
||||
## Metrics at a Glance
|
||||
| Metric | Your Value | Benchmark | Status |
|
||||
|--------|------------|-----------|--------|
|
||||
|
||||
## Overall Picture
|
||||
[2-3 sentences, plain English summary]
|
||||
|
||||
## Priority Issues
|
||||
|
||||
### 1. [Metric Name]
|
||||
What is happening: ...
|
||||
Why it matters: ...
|
||||
Fix it this month: ...
|
||||
|
||||
### 2. [Metric Name]
|
||||
...
|
||||
|
||||
## What is Working
|
||||
[1-2 genuine strengths, no padding]
|
||||
|
||||
## 90-Day Focus
|
||||
[Single metric to move + specific numeric target]
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
**Example 1 — Partial data**
|
||||
|
||||
Input: "MRR is $80k, we have 200 customers, about 3 cancel each month."
|
||||
|
||||
Expected output: Calculates ARPA ($400), monthly churn (1.5%), ARR ($960k), LTV estimate. Flags CAC and growth rate as missing. Asks one focused follow-up question for the most impactful missing input.
|
||||
|
||||
**Example 2 — Critical scenario**
|
||||
|
||||
Input: "MRR $22k (was $23.5k), 80 customers, lost 9, gained 6, spent $15k on ads, 65% gross margin."
|
||||
|
||||
Expected output: Flags negative MoM growth (-6.4%), critical churn (11.25%), and LTV:CAC of 0.64:1 as CRITICAL. Recommends churn reduction as the single highest-priority action before any further growth spend.
|
||||
|
||||
## Key Principles
|
||||
|
||||
- Be direct. If a metric is bad, say it is bad.
|
||||
- Explain every metric in one sentence before showing the number.
|
||||
- Cap priority issues at three. More than three paralyzes action.
|
||||
- Context changes benchmarks. Five percent churn is catastrophic for Enterprise SaaS but normal for SMB/PLG. Always confirm the user's target market before scoring.
|
||||
|
||||
## Reference Files
|
||||
|
||||
- `references/formulas.md` — All metric formulas with worked examples
|
||||
- `references/benchmarks.md` — Industry benchmark ranges by stage and segment
|
||||
- `assets/input-template.md` — Blank input form to share with users
|
||||
- `scripts/metrics_calculator.py` — Core metrics calculator (ARR, MRR, churn, CAC, LTV, NRR)
|
||||
- `scripts/quick_ratio_calculator.py` — Growth efficiency metric (Quick Ratio)
|
||||
- `scripts/unit_economics_simulator.py` — 12-month forward projection
|
||||
|
||||
## Tools
|
||||
|
||||
### 1. Metrics Calculator (`scripts/metrics_calculator.py`)
|
||||
Core SaaS metrics from raw business numbers.
|
||||
|
||||
```bash
|
||||
# Interactive mode
|
||||
python scripts/metrics_calculator.py
|
||||
|
||||
# CLI mode
|
||||
python scripts/metrics_calculator.py --mrr 50000 --customers 100 --churned 5 --json
|
||||
```
|
||||
|
||||
### 2. Quick Ratio Calculator (`scripts/quick_ratio_calculator.py`)
|
||||
Growth efficiency metric: (New MRR + Expansion) / (Churned + Contraction)
|
||||
|
||||
```bash
|
||||
python scripts/quick_ratio_calculator.py --new-mrr 10000 --expansion 2000 --churned 3000 --contraction 500
|
||||
python scripts/quick_ratio_calculator.py --new-mrr 10000 --expansion 2000 --churned 3000 --json
|
||||
```
|
||||
|
||||
**Benchmarks:**
|
||||
- < 1.0 = CRITICAL (losing faster than gaining)
|
||||
- 1-2 = WATCH (marginal growth)
|
||||
- 2-4 = HEALTHY (good efficiency)
|
||||
- \> 4 = EXCELLENT (strong growth)
|
||||
|
||||
### 3. Unit Economics Simulator (`scripts/unit_economics_simulator.py`)
|
||||
Project metrics forward 12 months based on growth/churn assumptions.
|
||||
|
||||
```bash
|
||||
python scripts/unit_economics_simulator.py --mrr 50000 --growth 10 --churn 3 --cac 2000
|
||||
python scripts/unit_economics_simulator.py --mrr 50000 --growth 10 --churn 3 --cac 2000 --json
|
||||
```
|
||||
|
||||
**Use for:**
|
||||
- "What if we grow at X% per month?"
|
||||
- Runway projections
|
||||
- Scenario planning (best/base/worst case)
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **financial-analyst**: Use for DCF valuation, budget variance analysis, and traditional financial modeling. NOT for SaaS-specific metrics like CAC, LTV, or churn.
|
||||
- **business-growth/customer-success**: Use for retention strategies and customer health scoring. Complements this skill when churn is flagged as CRITICAL.
|
||||
29
finance/saas-metrics-coach/assets/input-template.md
Normal file
29
finance/saas-metrics-coach/assets/input-template.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# SaaS Metrics — Input Template
|
||||
|
||||
Fill in what you know and paste to the SaaS Metrics Coach. Leave blanks empty.
|
||||
|
||||
---
|
||||
|
||||
**Context**
|
||||
- Target market: [ ] Enterprise [ ] Mid-Market [ ] SMB [ ] Consumer/PLG
|
||||
- Stage: [ ] Early (<$1M ARR) [ ] Growth ($1M–$10M) [ ] Scale ($10M+)
|
||||
|
||||
**Revenue**
|
||||
- Current MRR: $
|
||||
- MRR last month: $
|
||||
- Expansion MRR this month (upsells/upgrades): $
|
||||
- Churned MRR this month: $
|
||||
- Contraction MRR (downgrades): $
|
||||
|
||||
**Customers**
|
||||
- Total active customers:
|
||||
- New customers this month:
|
||||
- Churned customers this month:
|
||||
|
||||
**Costs**
|
||||
- Sales & Marketing spend this month: $
|
||||
- Gross margin %:
|
||||
- Net profit margin % (optional):
|
||||
|
||||
---
|
||||
*Partial data is fine — the coach works with whatever you have.*
|
||||
101
finance/saas-metrics-coach/references/benchmarks.md
Normal file
101
finance/saas-metrics-coach/references/benchmarks.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# SaaS Industry Benchmarks
|
||||
|
||||
Industry-standard benchmark ranges for SaaS metrics, segmented by company stage and market segment.
|
||||
|
||||
**Sources:**
|
||||
- OpenView SaaS Benchmarks 2024
|
||||
- Bessemer Venture Partners Cloud Index
|
||||
- SaaS Capital Index
|
||||
- Paddle SaaS Metrics Report 2025
|
||||
|
||||
**Last updated:** March 2026
|
||||
|
||||
## Stage Definitions
|
||||
|
||||
- Early: < $1M ARR
|
||||
- Growth: $1M–$10M ARR
|
||||
- Scale: $10M–$50M ARR
|
||||
- Late: $50M+ ARR
|
||||
|
||||
---
|
||||
|
||||
## Monthly Churn Rate
|
||||
|
||||
| Segment | CRITICAL | WATCH | HEALTHY |
|
||||
|---|---|---|---|
|
||||
| Enterprise (ACV > $25k) | > 3% | 1–3% | < 1% |
|
||||
| Mid-Market ($5k–$25k ACV) | > 5% | 2–5% | < 2% |
|
||||
| SMB / PLG (< $5k ACV) | > 8% | 4–8% | < 4% |
|
||||
| Consumer | > 10% | 5–10% | < 5% |
|
||||
|
||||
## LTV:CAC Ratio
|
||||
|
||||
| Status | Range |
|
||||
|---|---|
|
||||
| CRITICAL | < 1:1 — losing money on every customer |
|
||||
| POOR | 1:1–2:1 — barely breaking even |
|
||||
| WATCH | 2:1–3:1 — marginally viable |
|
||||
| HEALTHY | 3:1–5:1 — industry standard |
|
||||
| EXCELLENT | > 5:1 — strong unit economics |
|
||||
| WATCH | > 8:1 — possibly under-investing in growth |
|
||||
|
||||
## CAC Payback Period
|
||||
|
||||
| Status | Range |
|
||||
|---|---|
|
||||
| CRITICAL | > 24 months |
|
||||
| WATCH | 18–24 months |
|
||||
| HEALTHY | 12–18 months |
|
||||
| GOOD | 6–12 months |
|
||||
| EXCELLENT | < 6 months (PLG indicator) |
|
||||
|
||||
## NRR (Net Revenue Retention)
|
||||
|
||||
| Status | Range |
|
||||
|---|---|
|
||||
| CRITICAL | < 80% — revenue shrinking from existing base |
|
||||
| POOR | 80–90% |
|
||||
| WATCH | 90–100% — flat, not expanding |
|
||||
| HEALTHY | 100–110% |
|
||||
| EXCELLENT | 110–120% |
|
||||
| WORLD-CLASS | > 120% (Snowflake / Datadog territory) |
|
||||
|
||||
## MoM MRR Growth
|
||||
|
||||
| Stage | CRITICAL | WATCH | HEALTHY | EXCELLENT |
|
||||
|---|---|---|---|---|
|
||||
| Early (< $1M ARR) | < 5% | 5–10% | 10–20% | > 20% |
|
||||
| Growth ($1M–$10M) | < 3% | 3–7% | 7–15% | > 15% |
|
||||
| Scale ($10M+) | < 1% | 1–3% | 3–7% | > 7% |
|
||||
|
||||
## Gross Margin
|
||||
|
||||
| Status | Range |
|
||||
|---|---|
|
||||
| CRITICAL | < 50% |
|
||||
| WATCH | 50–65% |
|
||||
| HEALTHY | 65–75% |
|
||||
| EXCELLENT | 75–85% |
|
||||
| WORLD-CLASS | > 85% (API / infrastructure businesses) |
|
||||
|
||||
## Rule of 40
|
||||
|
||||
| Score | Status |
|
||||
|---|---|
|
||||
| < 20 | CONCERNING |
|
||||
| 20–40 | DEVELOPING |
|
||||
| 40–60 | HEALTHY |
|
||||
| > 60 | EXCELLENT |
|
||||
|
||||
## Quick Reference Card
|
||||
|
||||
```
|
||||
Metric Must Hit Good Great
|
||||
---------------------------------------------
|
||||
Monthly Churn < 5% < 3% < 1%
|
||||
LTV:CAC > 3:1 > 4:1 > 5:1
|
||||
CAC Payback < 18 mo < 12 mo < 6 mo
|
||||
NRR > 100% > 110% > 120%
|
||||
Gross Margin > 65% > 75% > 80%
|
||||
MoM Growth > 5% > 10% > 15%
|
||||
```
|
||||
103
finance/saas-metrics-coach/references/formulas.md
Normal file
103
finance/saas-metrics-coach/references/formulas.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# SaaS Metric Formulas
|
||||
|
||||
Complete reference with worked examples for all metrics calculated by the SaaS Metrics Coach.
|
||||
|
||||
## ARR (Annual Recurring Revenue)
|
||||
```
|
||||
ARR = MRR × 12
|
||||
```
|
||||
|
||||
**Example:**
|
||||
- Current MRR: $50,000
|
||||
- ARR = $50,000 × 12 = **$600,000**
|
||||
|
||||
**When to use:** Quick snapshot of annualized revenue run rate. Not the same as actual annual revenue if you have seasonality or one-time fees.
|
||||
|
||||
## MoM MRR Growth Rate
|
||||
```
|
||||
MoM Growth % = ((MRR_now - MRR_last) / MRR_last) × 100
|
||||
```
|
||||
|
||||
**Example:**
|
||||
- Current MRR: $50,000
|
||||
- Last month MRR: $45,000
|
||||
- Growth = (($50,000 - $45,000) / $45,000) × 100 = **11.1%**
|
||||
|
||||
**Interpretation:**
|
||||
- Negative = losing revenue
|
||||
- 0-5% = slow growth (concerning for early stage)
|
||||
- 5-15% = healthy growth
|
||||
- >15% = strong growth (early stage)
|
||||
|
||||
## Monthly Churn Rate
|
||||
```
|
||||
Churn % = (Customers lost / Customers at start of month) × 100
|
||||
```
|
||||
|
||||
**Example:**
|
||||
- Customers at start of month: 100
|
||||
- Customers lost during month: 5
|
||||
- Churn = (5 / 100) × 100 = **5%**
|
||||
|
||||
**Annualized impact:** 5% monthly = ~46% annual churn (compounding effect)
|
||||
|
||||
**Critical context:** Churn tolerance varies by segment:
|
||||
- Enterprise: >3% is critical
|
||||
- SMB: >8% is critical
|
||||
- Always confirm segment before judging severity
|
||||
|
||||
## ARPA (Avg Revenue Per Account)
|
||||
```
|
||||
ARPA = MRR / Total active customers
|
||||
```
|
||||
|
||||
## CAC (Customer Acquisition Cost)
|
||||
```
|
||||
CAC = Total Sales & Marketing spend / New customers acquired
|
||||
```
|
||||
Example: $20k spend / 10 customers → CAC $2,000
|
||||
|
||||
## LTV (Customer Lifetime Value)
|
||||
```
|
||||
LTV = (ARPA / Monthly Churn Rate) × Gross Margin %
|
||||
```
|
||||
|
||||
**Simplified (no gross margin data):**
|
||||
```
|
||||
LTV = ARPA / Monthly Churn Rate
|
||||
```
|
||||
|
||||
**Example:**
|
||||
- ARPA: $500
|
||||
- Monthly churn: 5% (0.05)
|
||||
- Gross margin: 70% (0.70)
|
||||
- LTV = ($500 / 0.05) × 0.70 = **$7,000**
|
||||
|
||||
**Simplified (no margin):** $500 / 0.05 = **$10,000**
|
||||
|
||||
**Why it matters:** LTV tells you the total revenue you can expect from an average customer. Must be at least 3x your CAC to have sustainable unit economics.
|
||||
|
||||
## LTV:CAC Ratio
|
||||
```
|
||||
LTV:CAC = LTV / CAC
|
||||
```
|
||||
Example: LTV $10k / CAC $2k = 5:1
|
||||
|
||||
## CAC Payback Period
|
||||
```
|
||||
Payback (months) = CAC / (ARPA × Gross Margin %)
|
||||
Simplified: Payback = CAC / ARPA
|
||||
```
|
||||
Example: CAC $2k / ARPA $500 = 4 months
|
||||
|
||||
## NRR (Net Revenue Retention)
|
||||
```
|
||||
NRR % = ((MRR_start + Expansion MRR - Churned MRR - Contraction MRR) / MRR_start) × 100
|
||||
```
|
||||
Simplified (no expansion data): NRR ≈ (1 - Revenue Churn Rate) × 100
|
||||
|
||||
## Rule of 40
|
||||
```
|
||||
Score = Annualized MoM Growth % + Net Profit Margin %
|
||||
Healthy: ≥ 40
|
||||
```
|
||||
217
finance/saas-metrics-coach/scripts/metrics_calculator.py
Normal file
217
finance/saas-metrics-coach/scripts/metrics_calculator.py
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/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)))
|
||||
173
finance/saas-metrics-coach/scripts/quick_ratio_calculator.py
Normal file
173
finance/saas-metrics-coach/scripts/quick_ratio_calculator.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick Ratio Calculator - SaaS growth efficiency metric.
|
||||
|
||||
Quick Ratio = (New MRR + Expansion MRR) / (Churned MRR + Contraction MRR)
|
||||
|
||||
A ratio > 4 indicates healthy, efficient growth.
|
||||
A ratio < 1 means you're losing revenue faster than gaining it.
|
||||
|
||||
Usage:
|
||||
python quick_ratio_calculator.py --new-mrr 10000 --expansion 2000 --churned 3000 --contraction 500
|
||||
python quick_ratio_calculator.py --new-mrr 10000 --expansion 2000 --churned 3000 --contraction 500 --json
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
|
||||
def calculate_quick_ratio(new_mrr, expansion_mrr, churned_mrr, contraction_mrr):
|
||||
"""
|
||||
Calculate Quick Ratio and provide interpretation.
|
||||
|
||||
Args:
|
||||
new_mrr: New MRR from new customers
|
||||
expansion_mrr: Expansion MRR from existing customers (upsells)
|
||||
churned_mrr: MRR lost from churned customers
|
||||
contraction_mrr: MRR lost from downgrades
|
||||
|
||||
Returns:
|
||||
dict with quick ratio and analysis
|
||||
"""
|
||||
# Calculate components
|
||||
growth_mrr = new_mrr + expansion_mrr
|
||||
lost_mrr = churned_mrr + contraction_mrr
|
||||
|
||||
# Quick Ratio
|
||||
if lost_mrr == 0:
|
||||
quick_ratio = float('inf') if growth_mrr > 0 else 0
|
||||
quick_ratio_display = "∞" if growth_mrr > 0 else "0"
|
||||
else:
|
||||
quick_ratio = growth_mrr / lost_mrr
|
||||
quick_ratio_display = f"{quick_ratio:.2f}"
|
||||
|
||||
# Status assessment
|
||||
if lost_mrr == 0 and growth_mrr > 0:
|
||||
status = "EXCELLENT"
|
||||
interpretation = "No revenue loss - perfect retention with growth"
|
||||
elif quick_ratio >= 4:
|
||||
status = "EXCELLENT"
|
||||
interpretation = "Strong, efficient growth - gaining revenue 4x faster than losing it"
|
||||
elif quick_ratio >= 2:
|
||||
status = "HEALTHY"
|
||||
interpretation = "Good growth efficiency - gaining revenue 2x+ faster than losing it"
|
||||
elif quick_ratio >= 1:
|
||||
status = "WATCH"
|
||||
interpretation = "Marginal growth - barely gaining more than losing"
|
||||
else:
|
||||
status = "CRITICAL"
|
||||
interpretation = "Losing revenue faster than gaining - growth is unsustainable"
|
||||
|
||||
# Breakdown percentages
|
||||
if growth_mrr > 0:
|
||||
new_pct = (new_mrr / growth_mrr) * 100
|
||||
expansion_pct = (expansion_mrr / growth_mrr) * 100
|
||||
else:
|
||||
new_pct = expansion_pct = 0
|
||||
|
||||
if lost_mrr > 0:
|
||||
churned_pct = (churned_mrr / lost_mrr) * 100
|
||||
contraction_pct = (contraction_mrr / lost_mrr) * 100
|
||||
else:
|
||||
churned_pct = contraction_pct = 0
|
||||
|
||||
results = {
|
||||
"quick_ratio": quick_ratio if quick_ratio != float('inf') else None,
|
||||
"quick_ratio_display": quick_ratio_display,
|
||||
"status": status,
|
||||
"interpretation": interpretation,
|
||||
"components": {
|
||||
"growth_mrr": round(growth_mrr, 2),
|
||||
"lost_mrr": round(lost_mrr, 2),
|
||||
"new_mrr": round(new_mrr, 2),
|
||||
"expansion_mrr": round(expansion_mrr, 2),
|
||||
"churned_mrr": round(churned_mrr, 2),
|
||||
"contraction_mrr": round(contraction_mrr, 2),
|
||||
},
|
||||
"breakdown": {
|
||||
"new_mrr_pct": round(new_pct, 1),
|
||||
"expansion_mrr_pct": round(expansion_pct, 1),
|
||||
"churned_mrr_pct": round(churned_pct, 1),
|
||||
"contraction_mrr_pct": round(contraction_pct, 1),
|
||||
},
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def format_report(results):
|
||||
"""Format quick ratio results as human-readable report."""
|
||||
lines = []
|
||||
lines.append("\n" + "=" * 70)
|
||||
lines.append("QUICK RATIO ANALYSIS")
|
||||
lines.append("=" * 70)
|
||||
|
||||
# Quick Ratio
|
||||
lines.append(f"\n⚡ QUICK RATIO: {results['quick_ratio_display']}")
|
||||
lines.append(f" Status: {results['status']}")
|
||||
lines.append(f" {results['interpretation']}")
|
||||
|
||||
# Components
|
||||
comp = results["components"]
|
||||
lines.append("\n📊 COMPONENTS")
|
||||
lines.append(f" Growth MRR (New + Expansion): ${comp['growth_mrr']:,.2f}")
|
||||
lines.append(f" • New MRR: ${comp['new_mrr']:,.2f}")
|
||||
lines.append(f" • Expansion MRR: ${comp['expansion_mrr']:,.2f}")
|
||||
lines.append(f" Lost MRR (Churned + Contraction): ${comp['lost_mrr']:,.2f}")
|
||||
lines.append(f" • Churned MRR: ${comp['churned_mrr']:,.2f}")
|
||||
lines.append(f" • Contraction MRR: ${comp['contraction_mrr']:,.2f}")
|
||||
|
||||
# Breakdown
|
||||
bd = results["breakdown"]
|
||||
lines.append("\n📈 GROWTH BREAKDOWN")
|
||||
lines.append(f" New customers: {bd['new_mrr_pct']:.1f}%")
|
||||
lines.append(f" Expansion: {bd['expansion_mrr_pct']:.1f}%")
|
||||
|
||||
lines.append("\n📉 LOSS BREAKDOWN")
|
||||
lines.append(f" Churn: {bd['churned_mrr_pct']:.1f}%")
|
||||
lines.append(f" Contraction: {bd['contraction_mrr_pct']:.1f}%")
|
||||
|
||||
# Benchmarks
|
||||
lines.append("\n🎯 BENCHMARKS")
|
||||
lines.append(" < 1.0 = CRITICAL (losing revenue faster than gaining)")
|
||||
lines.append(" 1-2 = WATCH (marginal growth)")
|
||||
lines.append(" 2-4 = HEALTHY (good growth efficiency)")
|
||||
lines.append(" > 4 = EXCELLENT (strong, efficient growth)")
|
||||
|
||||
lines.append("\n" + "=" * 70 + "\n")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Calculate SaaS Quick Ratio (growth efficiency metric)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--new-mrr", type=float, required=True, help="New MRR from new customers"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expansion", type=float, default=0, help="Expansion MRR from upsells (default: 0)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--churned", type=float, required=True, help="Churned MRR from lost customers"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--contraction", type=float, default=0, help="Contraction MRR from downgrades (default: 0)"
|
||||
)
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON format")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
results = calculate_quick_ratio(
|
||||
new_mrr=args.new_mrr,
|
||||
expansion_mrr=args.expansion,
|
||||
churned_mrr=args.churned,
|
||||
contraction_mrr=args.contraction,
|
||||
)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(results, indent=2))
|
||||
else:
|
||||
print(format_report(results))
|
||||
205
finance/saas-metrics-coach/scripts/unit_economics_simulator.py
Normal file
205
finance/saas-metrics-coach/scripts/unit_economics_simulator.py
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit Economics Simulator - Project SaaS metrics forward 12 months.
|
||||
|
||||
Usage:
|
||||
python unit_economics_simulator.py --mrr 50000 --growth 10 --churn 3 --cac 2000
|
||||
python unit_economics_simulator.py --mrr 50000 --growth 10 --churn 3 --cac 2000 --json
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
|
||||
def simulate(
|
||||
mrr,
|
||||
monthly_growth_pct,
|
||||
monthly_churn_pct,
|
||||
cac,
|
||||
gross_margin=0.70,
|
||||
sm_spend_pct=0.30,
|
||||
months=12,
|
||||
):
|
||||
"""
|
||||
Simulate unit economics forward.
|
||||
|
||||
Args:
|
||||
mrr: Starting MRR
|
||||
monthly_growth_pct: Expected monthly growth rate (%)
|
||||
monthly_churn_pct: Expected monthly churn rate (%)
|
||||
cac: Customer acquisition cost
|
||||
gross_margin: Gross margin (0-1)
|
||||
sm_spend_pct: Sales & marketing as % of revenue (0-1)
|
||||
months: Number of months to project
|
||||
|
||||
Returns:
|
||||
dict with monthly projections and summary
|
||||
"""
|
||||
results = {
|
||||
"inputs": {
|
||||
"starting_mrr": mrr,
|
||||
"monthly_growth_pct": monthly_growth_pct,
|
||||
"monthly_churn_pct": monthly_churn_pct,
|
||||
"cac": cac,
|
||||
"gross_margin": gross_margin,
|
||||
"sm_spend_pct": sm_spend_pct,
|
||||
},
|
||||
"projections": [],
|
||||
"summary": {},
|
||||
}
|
||||
|
||||
current_mrr = mrr
|
||||
cumulative_sm_spend = 0
|
||||
cumulative_gross_profit = 0
|
||||
|
||||
for month in range(1, months + 1):
|
||||
# Calculate growth and churn
|
||||
growth_rate = monthly_growth_pct / 100
|
||||
churn_rate = monthly_churn_pct / 100
|
||||
|
||||
# Net growth = growth - churn
|
||||
net_growth_rate = growth_rate - churn_rate
|
||||
new_mrr = current_mrr * (1 + net_growth_rate)
|
||||
|
||||
# Revenue and costs
|
||||
monthly_revenue = current_mrr
|
||||
gross_profit = monthly_revenue * gross_margin
|
||||
sm_spend = monthly_revenue * sm_spend_pct
|
||||
net_profit = gross_profit - sm_spend
|
||||
|
||||
# Accumulate
|
||||
cumulative_sm_spend += sm_spend
|
||||
cumulative_gross_profit += gross_profit
|
||||
|
||||
# ARR
|
||||
arr = current_mrr * 12
|
||||
|
||||
results["projections"].append({
|
||||
"month": month,
|
||||
"mrr": round(current_mrr, 2),
|
||||
"arr": round(arr, 2),
|
||||
"monthly_revenue": round(monthly_revenue, 2),
|
||||
"gross_profit": round(gross_profit, 2),
|
||||
"sm_spend": round(sm_spend, 2),
|
||||
"net_profit": round(net_profit, 2),
|
||||
"growth_rate_pct": round(net_growth_rate * 100, 2),
|
||||
})
|
||||
|
||||
current_mrr = new_mrr
|
||||
|
||||
# Summary
|
||||
final_mrr = results["projections"][-1]["mrr"]
|
||||
final_arr = results["projections"][-1]["arr"]
|
||||
total_revenue = sum(p["monthly_revenue"] for p in results["projections"])
|
||||
total_net_profit = sum(p["net_profit"] for p in results["projections"])
|
||||
|
||||
results["summary"] = {
|
||||
"starting_mrr": mrr,
|
||||
"ending_mrr": round(final_mrr, 2),
|
||||
"ending_arr": round(final_arr, 2),
|
||||
"mrr_growth_pct": round(((final_mrr - mrr) / mrr) * 100, 2),
|
||||
"total_revenue_12m": round(total_revenue, 2),
|
||||
"total_gross_profit_12m": round(cumulative_gross_profit, 2),
|
||||
"total_sm_spend_12m": round(cumulative_sm_spend, 2),
|
||||
"total_net_profit_12m": round(total_net_profit, 2),
|
||||
"avg_monthly_growth_pct": round((monthly_growth_pct - monthly_churn_pct), 2),
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def format_report(results):
|
||||
"""Format simulation results as human-readable report."""
|
||||
lines = []
|
||||
lines.append("\n" + "=" * 70)
|
||||
lines.append("UNIT ECONOMICS SIMULATION - 12 MONTH PROJECTION")
|
||||
lines.append("=" * 70)
|
||||
|
||||
# Inputs
|
||||
inputs = results["inputs"]
|
||||
lines.append("\n📊 INPUTS")
|
||||
lines.append(f" Starting MRR: ${inputs['starting_mrr']:,.0f}")
|
||||
lines.append(f" Monthly Growth: {inputs['monthly_growth_pct']}%")
|
||||
lines.append(f" Monthly Churn: {inputs['monthly_churn_pct']}%")
|
||||
lines.append(f" CAC: ${inputs['cac']:,.0f}")
|
||||
lines.append(f" Gross Margin: {inputs['gross_margin']*100:.0f}%")
|
||||
lines.append(f" S&M Spend: {inputs['sm_spend_pct']*100:.0f}% of revenue")
|
||||
|
||||
# Summary
|
||||
summary = results["summary"]
|
||||
lines.append("\n📈 12-MONTH SUMMARY")
|
||||
lines.append(f" Starting MRR: ${summary['starting_mrr']:,.0f}")
|
||||
lines.append(f" Ending MRR: ${summary['ending_mrr']:,.0f}")
|
||||
lines.append(f" Ending ARR: ${summary['ending_arr']:,.0f}")
|
||||
lines.append(f" MRR Growth: {summary['mrr_growth_pct']:+.1f}%")
|
||||
lines.append(f" Total Revenue: ${summary['total_revenue_12m']:,.0f}")
|
||||
lines.append(f" Total Gross Profit: ${summary['total_gross_profit_12m']:,.0f}")
|
||||
lines.append(f" Total S&M Spend: ${summary['total_sm_spend_12m']:,.0f}")
|
||||
lines.append(f" Total Net Profit: ${summary['total_net_profit_12m']:,.0f}")
|
||||
|
||||
# Monthly breakdown (first 3, last 3)
|
||||
lines.append("\n📅 MONTHLY PROJECTIONS")
|
||||
lines.append(f"{'Month':<8} {'MRR':<12} {'ARR':<12} {'Revenue':<12} {'Net Profit':<12}")
|
||||
lines.append("-" * 70)
|
||||
|
||||
projs = results["projections"]
|
||||
for p in projs[:3]:
|
||||
lines.append(
|
||||
f"{p['month']:<8} ${p['mrr']:<11,.0f} ${p['arr']:<11,.0f} "
|
||||
f"${p['monthly_revenue']:<11,.0f} ${p['net_profit']:<11,.0f}"
|
||||
)
|
||||
|
||||
if len(projs) > 6:
|
||||
lines.append(" ...")
|
||||
|
||||
for p in projs[-3:]:
|
||||
lines.append(
|
||||
f"{p['month']:<8} ${p['mrr']:<11,.0f} ${p['arr']:<11,.0f} "
|
||||
f"${p['monthly_revenue']:<11,.0f} ${p['net_profit']:<11,.0f}"
|
||||
)
|
||||
|
||||
lines.append("\n" + "=" * 70 + "\n")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Simulate SaaS unit economics over 12 months"
|
||||
)
|
||||
parser.add_argument("--mrr", type=float, required=True, help="Starting MRR")
|
||||
parser.add_argument(
|
||||
"--growth", type=float, required=True, help="Monthly growth rate (%)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--churn", type=float, required=True, help="Monthly churn rate (%)"
|
||||
)
|
||||
parser.add_argument("--cac", type=float, required=True, help="Customer acquisition cost")
|
||||
parser.add_argument(
|
||||
"--gross-margin", type=float, default=70, help="Gross margin %% (default: 70)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sm-spend", type=float, default=30, help="S&M spend as %% of revenue (default: 30)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--months", type=int, default=12, help="Months to project (default: 12)"
|
||||
)
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON format")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
results = simulate(
|
||||
mrr=args.mrr,
|
||||
monthly_growth_pct=args.growth,
|
||||
monthly_churn_pct=args.churn,
|
||||
cac=args.cac,
|
||||
gross_margin=args.gross_margin / 100 if args.gross_margin > 1 else args.gross_margin,
|
||||
sm_spend_pct=args.sm_spend / 100 if args.sm_spend > 1 else args.sm_spend,
|
||||
months=args.months,
|
||||
)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(results, indent=2))
|
||||
else:
|
||||
print(format_report(results))
|
||||
Reference in New Issue
Block a user