AXLR / Catalog / finance

break_even

Cost-volume-profit analysis: break-even units and revenue, contribution margin and ratio, margin of safety, operating income and degree of operating leverage at a sales level, units for a target profit before or after tax, a sales-mix break-even for several products (mix by units or by revenue), and a sensitivity table over price, variable-cost and fixed-cost changes. Decimal arithmetic.

Install

/tools install break_even
/tools install break_even@1.0.0

In a console session, or let the model find it in normal mode: axlr_catalog describes it and axlr_install_tool asks for the same install card. The card reads catalog axlr · break_even 1.0.0 · verified once the index signature, the manifest digest and every file digest check out. Then the model runs it with axlr_run_tool break_even; each run is approved as a local_exec.

Arguments

One JSON object on stdin, validated against the schema before the program starts.

nametyperequireddefaultmeaning
fixed_costsnumber | stringyes
pricenumber | stringnoUnit price, one product
variable_costnumber | stringnoUnit variable cost, one product
productsarray of objectnoitems ≥ 1
mix_basisstringno"units"one of "units", "revenue"
sales_unitsnumber | stringnoExpected units, one product
sales_revenuenumber | stringno
target_profitnumber | stringno
tax_ratenumber | stringnoMakes target_profit after tax: 0.25 or "25%"
sensitivityobjectno
Input schema (JSON)
{
  "type": "object",
  "required": [
    "fixed_costs"
  ],
  "additionalProperties": false,
  "properties": {
    "fixed_costs": {
      "type": [
        "number",
        "string"
      ]
    },
    "price": {
      "type": [
        "number",
        "string"
      ],
      "description": "Unit price, one product"
    },
    "variable_cost": {
      "type": [
        "number",
        "string"
      ],
      "description": "Unit variable cost, one product"
    },
    "products": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": [
          "price",
          "variable_cost",
          "mix"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "price": {
            "type": [
              "number",
              "string"
            ]
          },
          "variable_cost": {
            "type": [
              "number",
              "string"
            ]
          },
          "mix": {
            "type": [
              "number",
              "string"
            ],
            "description": "Share of units or of revenue, any scale"
          }
        }
      }
    },
    "mix_basis": {
      "type": "string",
      "enum": [
        "units",
        "revenue"
      ],
      "default": "units"
    },
    "sales_units": {
      "type": [
        "number",
        "string"
      ],
      "description": "Expected units, one product"
    },
    "sales_revenue": {
      "type": [
        "number",
        "string"
      ]
    },
    "target_profit": {
      "type": [
        "number",
        "string"
      ]
    },
    "tax_rate": {
      "type": [
        "number",
        "string"
      ],
      "description": "Makes target_profit after tax: 0.25 or \"25%\""
    },
    "sensitivity": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "price_changes_pct": {
          "type": "array",
          "items": {
            "type": [
              "number",
              "string"
            ]
          },
          "maxItems": 25
        },
        "variable_cost_changes_pct": {
          "type": "array",
          "items": {
            "type": [
              "number",
              "string"
            ]
          },
          "maxItems": 25
        },
        "fixed_cost_changes_pct": {
          "type": "array",
          "items": {
            "type": [
              "number",
              "string"
            ]
          },
          "maxItems": 25
        }
      }
    }
  }
}

Example

Input

{
  "fixed_costs": 35000,
  "price": 250,
  "variable_cost": 150,
  "sales_units": 400
}

Output (stdout)

{
  "break_even_units": {
    "exact": "350.0000",
    "rounded_up": "350"
  },
  "break_even_revenue": "87500.00",
  "at_sales": {
    "margin_of_safety_pct": "12.50",
    "degree_of_operating_leverage": "8.0000"
  }
}

Files

The command the install card registers is python3 break_even.py, run in the workspace with PATH and HOME as its only environment. Each file's SHA-256 is in the signed index; the console checks them before showing the card and again before writing.

break_even.py · sha256 a22ea751a81efd424ff67995917c084c2d560a8081850e261a7d73706ed0f40b
#!/usr/bin/env python3
"""Cost-volume-profit analysis in Decimal: break-even units and revenue, contribution margin
and ratio, margin of safety, operating income and degree of operating leverage at a sales
level, the units for a target profit (before or after tax), a sales-mix break-even for several
products, and a sensitivity table of the break-even over price and variable-cost changes.

Input (JSON on stdin):
  fixed_costs     total fixed costs of the period
  price           unit selling price                   one product
  variable_cost   unit variable cost                   one product
  products        [{"name", "price", "variable_cost", "mix"}]   several products instead;
                  mix is a share (any scale) of units or of revenue, see mix_basis
  mix_basis       "units" (default) | "revenue"
  sales_units     expected sales in units (one product)       for margin of safety and leverage
  sales_revenue   expected sales revenue (either case)
  target_profit   operating profit wanted
  tax_rate        with target_profit as an after-tax figure: 0.25 or "25%"
  sensitivity     {"price_changes_pct": [-10, 0, 10], "variable_cost_changes_pct": [0, 5],
                   "fixed_cost_changes_pct": [0]}   one product
"""
import json
import sys
from decimal import ROUND_CEILING, ROUND_HALF_UP, Decimal, InvalidOperation, getcontext

getcontext().prec = 40


def fail(message):
    print(json.dumps({"error": message}))
    sys.exit(2)


def dec(value, field, percent=False):
    if isinstance(value, bool) or not isinstance(value, (int, float, str)):
        fail(f"{field} must be a number or a decimal string")
    text = str(value).strip()
    scale = Decimal(1)
    if percent and text.endswith("%"):
        text, scale = text[:-1], Decimal("0.01")
    try:
        number = Decimal(text) * scale
    except InvalidOperation:
        fail(f"{field} must be a number or a decimal string")
    if not number.is_finite():
        fail(f"{field} must be finite")
    return number


def q(x, places=2):
    return format(x.quantize(Decimal(1).scaleb(-places), ROUND_HALF_UP), "f")


def units_out(x):
    return {"exact": q(x, 4), "rounded_up": format(x.to_integral_value(ROUND_CEILING), "f")}


def check_product(price, variable, label):
    if price <= 0:
        fail(f"{label}price must be positive")
    if variable < 0:
        fail(f"{label}variable_cost must not be negative")
    if price <= variable:
        fail(f"{label}price must exceed variable_cost: no contribution margin, no break-even")


def main():
    try:
        request = json.load(sys.stdin)
    except ValueError as error:
        fail(f"stdin is not JSON: {error}")
    if not isinstance(request, dict):
        fail("input must be a JSON object")
    known = {"fixed_costs", "price", "variable_cost", "products", "mix_basis", "sales_units", "sales_revenue",
             "target_profit", "tax_rate", "sensitivity"}
    extra = sorted(set(request) - known)
    if extra:
        fail(f"unknown field: {extra[0]}")
    if "fixed_costs" not in request:
        fail("fixed_costs is required")
    fixed = dec(request["fixed_costs"], "fixed_costs")
    if fixed < 0:
        fail("fixed_costs must not be negative")
    single = "products" not in request
    if single and ("price" not in request or "variable_cost" not in request):
        fail("give price and variable_cost, or products")
    if not single and ("price" in request or "variable_cost" in request):
        fail("give either price and variable_cost or products, not both")
    result = {"fixed_costs": q(fixed)}

    if single:
        price = dec(request["price"], "price")
        variable = dec(request["variable_cost"], "variable_cost")
        check_product(price, variable, "")
        cm = price - variable
        cm_ratio = cm / price
        be_units = fixed / cm
        be_revenue = be_units * price
        result.update({"contribution_margin_per_unit": q(cm), "contribution_margin_ratio": q(cm_ratio, 6),
                       "break_even_units": units_out(be_units), "break_even_revenue": q(be_revenue)})
    else:
        basis = request.get("mix_basis", "units")
        if basis not in ("units", "revenue"):
            fail("mix_basis must be units or revenue")
        raw = request["products"]
        if not isinstance(raw, list) or not raw:
            fail("products must be a non-empty list")
        products = []
        for i, item in enumerate(raw):
            if not isinstance(item, dict) or set(item) - {"name", "price", "variable_cost", "mix"}:
                fail(f"products[{i}] must be an object with name, price, variable_cost and mix")
            for key in ("price", "variable_cost", "mix"):
                if key not in item:
                    fail(f"products[{i}].{key} is required")
            p = dec(item["price"], f"products[{i}].price")
            v = dec(item["variable_cost"], f"products[{i}].variable_cost")
            check_product(p, v, f"products[{i}].")
            mix = dec(item["mix"], f"products[{i}].mix")
            if mix < 0:
                fail(f"products[{i}].mix must not be negative")
            products.append({"name": str(item.get("name", f"product {i + 1}")), "price": p, "variable": v, "mix": mix})
        total_mix = sum(p["mix"] for p in products)
        if total_mix <= 0:
            fail("the mix shares must not all be zero")
        if basis == "units":
            weighted_cm = sum(p["mix"] * (p["price"] - p["variable"]) for p in products) / total_mix
            weighted_price = sum(p["mix"] * p["price"] for p in products) / total_mix
            cm_ratio = weighted_cm / weighted_price
            be_units_total = fixed / weighted_cm
            be_revenue = be_units_total * weighted_price
            rows = [{"name": p["name"], "mix_share": q(p["mix"] / total_mix, 6), "units": units_out(be_units_total * p["mix"] / total_mix),
                     "revenue": q(be_units_total * p["mix"] / total_mix * p["price"])} for p in products]
            result["weighted_contribution_margin_per_unit"] = q(weighted_cm, 4)
            result["break_even_units"] = units_out(be_units_total)
        else:
            cm_ratio = sum(p["mix"] * (p["price"] - p["variable"]) / p["price"] for p in products) / total_mix
            be_revenue = fixed / cm_ratio
            rows = []
            for p in products:
                revenue = be_revenue * p["mix"] / total_mix
                rows.append({"name": p["name"], "mix_share": q(p["mix"] / total_mix, 6), "units": units_out(revenue / p["price"]), "revenue": q(revenue)})
        result.update({"mix_basis": basis, "contribution_margin_ratio": q(cm_ratio, 6), "break_even_revenue": q(be_revenue), "products": rows})

    if "sales_units" in request or "sales_revenue" in request:
        if "sales_units" in request and "sales_revenue" in request:
            fail("give sales_units or sales_revenue, not both")
        if "sales_units" in request:
            if not single:
                fail("with several products give sales_revenue")
            units = dec(request["sales_units"], "sales_units")
            if units < 0:
                fail("sales_units must not be negative")
            revenue = units * price
        else:
            revenue = dec(request["sales_revenue"], "sales_revenue")
            if revenue < 0:
                fail("sales_revenue must not be negative")
        contribution = revenue * cm_ratio
        income = contribution - fixed
        safety = {"revenue": q(revenue), "contribution_margin": q(contribution), "operating_income": q(income),
                  "margin_of_safety": q(revenue - be_revenue),
                  "margin_of_safety_pct": q((revenue - be_revenue) / revenue * 100) if revenue else None}
        if single:
            safety["margin_of_safety_units"] = q((revenue - be_revenue) / price, 4)
        safety["degree_of_operating_leverage"] = q(contribution / income, 4) if income != 0 else None
        if income <= 0:
            safety["note"] = "at or below break-even: operating leverage is not meaningful"
        result["at_sales"] = safety

    if "target_profit" in request:
        target = dec(request["target_profit"], "target_profit")
        before_tax = target
        out = {"target_profit": q(target)}
        if "tax_rate" in request:
            tax = dec(request["tax_rate"], "tax_rate", percent=True)
            if not 0 <= tax < 1:
                fail("tax_rate must be from 0 to below 100%")
            before_tax = target / (1 - tax)
            out["tax_rate"] = format(tax, "f")
            out["profit_before_tax"] = q(before_tax)
        needed_revenue = (fixed + before_tax) / cm_ratio
        out["revenue"] = q(needed_revenue)
        if single:
            out["units"] = units_out((fixed + before_tax) / cm)
        result["target"] = out
    elif "tax_rate" in request:
        fail("tax_rate needs target_profit")

    if "sensitivity" in request:
        if not single:
            fail("sensitivity works on one product (price and variable_cost)")
        grid = request["sensitivity"]
        allowed = {"price_changes_pct", "variable_cost_changes_pct", "fixed_cost_changes_pct"}
        if not isinstance(grid, dict) or set(grid) - allowed:
            fail("sensitivity must be an object with price_changes_pct, variable_cost_changes_pct, fixed_cost_changes_pct")
        axes = {}
        for key in sorted(allowed):
            values = grid.get(key, [0])
            if not isinstance(values, list) or not values or len(values) > 25:
                fail(f"sensitivity.{key} must be a list of 1 to 25 percent changes")
            axes[key] = [dec(v, f"sensitivity.{key}[]") for v in values]
        table = []
        for fc in axes["fixed_cost_changes_pct"]:
            for pc in axes["price_changes_pct"]:
                for vc in axes["variable_cost_changes_pct"]:
                    p = price * (1 + pc / 100)
                    v = variable * (1 + vc / 100)
                    f = fixed * (1 + fc / 100)
                    row = {"price_change_pct": format(pc.normalize(), "f"), "variable_cost_change_pct": format(vc.normalize(), "f"),
                           "fixed_cost_change_pct": format(fc.normalize(), "f"), "price": q(p), "variable_cost": q(v)}
                    if p <= v or p <= 0:
                        row.update({"break_even_units": None, "break_even_revenue": None, "note": "no contribution margin"})
                    else:
                        row.update({"break_even_units": units_out(f / (p - v)), "break_even_revenue": q(f / (p - v) * p)})
                    table.append(row)
        result["sensitivity"] = table
    print(json.dumps(result))


if __name__ == "__main__":
    try:
        main()
    except ArithmeticError:
        fail("the numbers are out of the range this tool can compute")

Where it is served

The registry serves only AXLR clients, so a browser gets 403; fetch with a User-Agent that starts with axlr:

curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/break_even/1.0.0/manifest.json
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/break_even/1.0.0/break_even.py