Investment Analysis with NPV and IRR

BusinessMath Quarterly Series

17 min read

Part 17 of 12-Week BusinessMath Series


What You’ll Learn


The Problem

Every business faces investment decisions: Should we expand into a new market? Buy this equipment? Acquire that company? Bad investment decisions destroy value: Spreadsheet investment analysis is error-prone and doesn’t scale when evaluating dozens of opportunities.

The Solution

BusinessMath provides comprehensive investment analysis functions: npv(), irr(), xnpv(), xirr(), plus supporting metrics like profitability index and payback periods.

Define the Investment

Let’s analyze a rental property investment:
import BusinessMath
import Foundation

// Rental property opportunity
let propertyPrice = 250_000.0
let downPayment = 50_000.0 // 20% down
let renovationCosts = 20_000.0
let initialInvestment = downPayment + renovationCosts // $70,000

// Expected annual cash flows (after expenses and mortgage)
let year1 = 8_000.0
let year2 = 8_500.0
let year3 = 9_000.0
let year4 = 9_500.0
let year5 = 10_000.0
let salePrice = 300_000.0 // Sell after 5 years
let mortgagePayoff = 190_000.0
let saleProceeds = salePrice - mortgagePayoff // Net: $110,000

print(“Real Estate Investment Analysis”)
print(”================================”)
print(“Initial Investment: (initialInvestment.currency(0))”)
print(” Down Payment: (downPayment.currency(0))”)
print(” Renovations: (renovationCosts.currency(0))”)
print(”\nExpected Cash Flows:”)
print(” Years 1-5: Annual rental income”)
print(” Year 5: + Sale proceeds ((saleProceeds.currency(0)))”)
print(” Required Return: 12%”)
Output:
Real Estate Investment Analysis
================================
Initial Investment: $70,000
Down Payment: $50,000
Renovations: $20,000

Expected Cash Flows:
Years 1-5: Annual rental income
Year 5: + Sale proceeds ($110,000)
Required Return: 12%

Calculate NPV

Determine if the investment creates value at your required return:
// Define all cash flows
let cashFlows = [
-initialInvestment, // Year 0: Outflow
year1, // Year 1: Rental income
year2, // Year 2
year3, // Year 3
year4, // Year 4
year5 + saleProceeds // Year 5: Rental + sale
]

let requiredReturn = 0.12
let npvValue = npv(discountRate: requiredReturn, cashFlows: cashFlows)

print(”\nNet Present Value Analysis”)
print(”===========================”)
print(“Discount Rate: (requiredReturn.percent())”)
print(“NPV: (npvValue.currency(0))”)

if npvValue > 0 {
print(“✓ Positive NPV - Investment adds value”)
print(” For every $1 invested, you create ((1 + npvValue / initialInvestment).currency(2)) of value”)
} else if npvValue < 0 {
print(“✗ Negative NPV - Investment destroys value”)
print(” Should reject this opportunity”)
} else {
print(“○ Zero NPV - Breakeven investment”)
}
Output:
Net Present Value Analysis
===========================
Discount Rate: 12.00%
NPV: $24,454
✓ Positive NPV - Investment adds value
For every $1 invested, you create $1.35 of value
The decision rule: NPV > 0 means accept. This investment creates $24,454 of value at your 12% required return.

Calculate IRR

Find the actual return rate of the investment:
let irrValue = try irr(cashFlows: cashFlows)

print(”\nInternal Rate of Return”)
print(”=======================”)
print(“IRR: (irrValue.percent(2))”)
print(“Required Return: (requiredReturn.percent())”)

if irrValue > requiredReturn {
let spread = (irrValue - requiredReturn) * 100
print(“✓ IRR exceeds required return by (spread.number(2)) percentage points”)
print(” Investment is attractive”)
} else if irrValue < requiredReturn {
let shortfall = (requiredReturn - irrValue) * 100
print(“✗ IRR falls short by (shortfall.number(2)) percentage points”)
} else {
print(“○ IRR equals required return - Breakeven”)
}

// Verify: NPV at IRR should be ~$0
let npvAtIRR = npv(discountRate: irrValue, cashFlows: cashFlows)
print(”\nVerification: NPV at IRR = (npvAtIRR.currency()) (should be ~$0)”)
Output:
Internal Rate of Return
=======================
IRR: 20.24%
Required Return: 12.00%
✓ IRR exceeds required return by 8.24 percentage points
Investment is attractive

Verification: NPV at IRR = $0.00 (should be ~$0)

**The insight**: The investment returns **22.83%**, well above the 12% hurdle rate. IRR is the discount rate that makes NPV = $0.

---

### Additional Investment Metrics

Calculate supporting metrics for a complete picture:

swift
// Profitability Index
let pi = profitabilityIndex(rate: requiredReturn, cashFlows: cashFlows)

print(”\nProfitability Index”)
print(”===================”)
print(“PI: (pi.number(2))”)
if pi > 1.0 {
print(“✓ PI > 1.0 - Creates value”)
print(” Returns (pi.currency(2)) for every $1 invested”)
} else {
print(“✗ PI < 1.0 - Destroys value”)
}

// Payback Period
let payback = paybackPeriod(cashFlows: cashFlows)

print(”\nPayback Period”)
print(”==============”)
if let pb = payback {
print(“Simple Payback: (pb) years”)
print(” Investment recovered in year (pb)”)
} else {
print(“Investment never recovers initial outlay”)
}

// Discounted Payback
let discountedPayback = discountedPaybackPeriod(
rate: requiredReturn,
cashFlows: cashFlows
)

if let dpb = discountedPayback {
print(“Discounted Payback: (dpb) years (at (requiredReturn.percent()))”)
if let pb = payback {
let difference = dpb - pb
print(” Takes (difference) more years accounting for time value”)
}
}
Output:
Profitability Index
===================
PI: 1.35
✓ PI > 1.0 - Creates value
Returns $1.35 for every $1 invested

Payback Period
==============
Simple Payback: 5 years
Investment recovered in year 5

Discounted Payback: 5 years (at 12.00%)
Takes 0 more years accounting for time value
The metrics:

Sensitivity Analysis

Test how changes in assumptions affect the decision:
print(”\nSensitivity Analysis”)
print(”====================”)

// Test different discount rates
print(“NPV at Different Discount Rates:”)
print(“Rate | NPV | Decision”)
print(”——|————|–––––”)

for rate in stride(from: 0.08, through: 0.16, by: 0.02) {
let npv = npv(discountRate: rate, cashFlows: cashFlows)
let decision = npv > 0 ? “Accept” : “Reject”
print(”(rate.percent(0)) | (npv.currency()) | (decision)”)
}

// Test different sale prices
print(”\nNPV at Different Sale Prices:”)
print(“Sale Price | Net Proceeds | NPV | Decision”)
print(”———–|–––––––|————|–––––”)

for price in stride(from: 240_000.0, through: 340_000.0, by: 20_000.0) {
let proceeds = price - mortgagePayoff
let flows = [-initialInvestment, year1, year2, year3, year4, year5 + proceeds]
let npv = npv(discountRate: requiredReturn, cashFlows: flows)
let decision = npv > 0 ? “Accept” : “Reject”
print(”(price.currency(0)) | (proceeds.currency(0)) | (npv.currency()) | (decision)”)
}
Output:
Sensitivity Analysis
====================
NPV at Different Discount Rates:
Rate | NPV | Decision
——|————|–––––
8% | $40,492 | Accept
10% | $32,059 | Accept
12% | $24,454 | Accept
14% | $17,582 | Accept
16% | $11,360 | Accept

NPV at Different Sale Prices:
Sale Price | Net Proceeds | NPV | Decision
———–|–––––––|————|–––––
$240,000 | $50,000 | ($9,592) | Reject
$260,000 | $70,000 | $1,757 | Accept
$280,000 | $90,000 | $13,105 | Accept
$300,000 | $110,000 | $24,454 | Accept
$320,000 | $130,000 | $35,802 | Accept
$340,000 | $150,000 | $47,151 | Accept
The risk assessment: The investment is sensitive to sale price. If the property sells for < ~$260k, NPV turns negative. This is your margin of safety.

Breakeven Analysis

Find the exact breakeven sale price where NPV = $0:
print(”\nBreakeven Analysis:”)

var low = 200_000.0
var high = 350_000.0
var breakeven = (low + high) / 2

// Binary search for breakeven
for _ in 0..<20 {
let proceeds = breakeven - mortgagePayoff
let flows = [-initialInvestment, year1, year2, year3, year4, year5 + proceeds]
let npv = npv(discountRate: requiredReturn, cashFlows: flows)

if abs(npv) < 1.0 { break } // Close enough
else if npv > 0 { high = breakeven }
else { low = breakeven }

breakeven = (low + high) / 2
}

print(“Breakeven Sale Price: (breakeven.currency(0))”)
print(” At this price, NPV = $0 and IRR = (requiredReturn.percent())”)
print(” Current assumption: (salePrice.currency(0))”)
print(” Safety margin: ((salePrice - breakeven).currency(0)) ((((salePrice - breakeven) / salePrice).percent(1)))”)
Output:
Breakeven Analysis:
Breakeven Sale Price: $256,905
At this price, NPV = $0 and IRR = 12.00%
Current assumption: $300,000
Safety margin: $43,095 (14.4%)
The cushion: The property can drop $43k (14.4%) from your expected sale price before the investment turns negative.

Compare Multiple Investments

Rank several opportunities systematically:
print(”\nComparing Investment Opportunities”)
print(”===================================”)

struct Investment {
let name: String
let cashFlows: [Double]
let description: String
}

let investments = [
Investment(
name: “Real Estate”,
cashFlows: [-70_000, 8_000, 8_500, 9_000, 9_500, 120_000],
description: “Rental property with 5-year hold”
),
Investment(
name: “Stock Portfolio”,
cashFlows: [-70_000, 5_000, 5_500, 6_000, 6_500, 75_000],
description: “Diversified equity portfolio”
),
Investment(
name: “Business Expansion”,
cashFlows: [-70_000, 0, 10_000, 15_000, 20_000, 40_000],
description: “Expand product line (delayed returns)”
)
]

print(”\nInvestment | NPV | IRR | PI | Payback”)
print(”——————|———–|———|——|––––”)

var results: [(name: String, npv: Double, irr: Double)] = []

for investment in investments {
let npv = npv(discountRate: requiredReturn, cashFlows: investment.cashFlows)
let irr = try irr(cashFlows: investment.cashFlows)
let pi = profitabilityIndex(rate: requiredReturn, cashFlows: investment.cashFlows)
let pb = paybackPeriod(cashFlows: investment.cashFlows) ?? 99

results.append((investment.name, npv, irr))
print(”(investment.name.padding(toLength: 17, withPad: “ “, startingAt: 0)) | (npv.currency(0)) | (irr.percent(1)) | (pi.number(2)) | (pb) yrs”)
}

// Rank by NPV
let ranked = results.sorted { $0.npv > $1.npv }

print(”\nRanking by NPV:”)
for (i, result) in ranked.enumerated() {
print(” (i + 1). (result.name) - NPV: (result.npv.currency(0))”)
}

print(”\nRecommendation: Choose ‘(ranked[0].name)’”)
print(” Highest NPV = Maximum value creation”)
Output:
Comparing Investment Opportunities
===================================

Investment | NPV | IRR | PI | Payback
——————|———–|———|——|––––
Real Estate | $24,454 | 20.2% | 1.35 | 5 yrs
Stock Portfolio | ($10,193) | 8.0% | 0.85 | 5 yrs
Business Expansio | ($15,944) | 4.9% | 0.77 | 5 yrs

Ranking by NPV:
1. Real Estate - NPV: $24,454
2. Stock Portfolio - NPV: ($10,193)
3. Business Expansion - NPV: ($15,944)

Recommendation: Choose ‘Real Estate’
Highest NPV = Maximum value creation
The decision: Real Estate has the highest NPV, creating $24,454 of value. Even though Business Expansion has a higher IRR than Stock Portfolio, Real Estate wins on absolute value creation.

Irregular Cash Flow Analysis

Use XNPV and XIRR for real-world irregular timing:
print(”\nIrregular Cash Flow Analysis”)
print(”============================”)

let startDate = Date()
let dates = [
startDate, // Today: Initial investment
startDate.addingTimeInterval(90 * 86400), // 90 days
startDate.addingTimeInterval(250 * 86400), // 250 days
startDate.addingTimeInterval(400 * 86400), // 400 days
startDate.addingTimeInterval(600 * 86400), // 600 days
startDate.addingTimeInterval(5 * 365 * 86400) // 5 years
]

let irregularFlows = [-70_000.0, 8_000, 8_500, 9_000, 9_500, 120_000]

// XNPV accounts for exact dates
let xnpvValue = try xnpv(rate: requiredReturn, dates: dates, cashFlows: irregularFlows)
print(“XNPV (irregular timing): (xnpvValue.currency())”)

// XIRR finds return with irregular dates
let xirrValue = try xirr(dates: dates, cashFlows: irregularFlows)
print(“XIRR (irregular timing): (xirrValue.percent(2))”)

// Compare to regular IRR (assumes annual periods)
let regularIRR = try irr(cashFlows: irregularFlows)
print(”\nComparison:”)
print(” Regular IRR (annual periods): (regularIRR.percent(2))”)
print(” XIRR (actual dates): (xirrValue.percent(2))”)
print(” Difference: (((xirrValue - regularIRR) * 10000).number(0)) basis points”)
Output:
Irregular Cash Flow Analysis
============================
XNPV (irregular timing): $29,570.08
XIRR (irregular timing): 23.80%

Comparison:
Regular IRR (annual periods): 20.24%
XIRR (actual dates): 23.80%
Difference: 356 basis points
The precision: XIRR is more accurate for real-world investments where cash flows don’t arrive exactly annually.

Try It Yourself

Click to expand full playground code
import BusinessMath
import Foundation

// Rental property opportunity
let propertyPrice = 250_000.0
let downPayment = 50_000.0 // 20% down
let renovationCosts = 20_000.0
let initialInvestment = downPayment + renovationCosts // $70,000

// Expected annual cash flows (after expenses and mortgage)
let year1 = 8_000.0
let year2 = 8_500.0
let year3 = 9_000.0
let year4 = 9_500.0
let year5 = 10_000.0
let salePrice = 300_000.0 // Sell after 5 years
let mortgagePayoff = 190_000.0
let saleProceeds = salePrice - mortgagePayoff // Net: $110,000

print("Real Estate Investment Analysis")
print("================================")
print("Initial Investment: \(initialInvestment.currency(0))")
print(" Down Payment: \(downPayment.currency(0))")
print(" Renovations: \(renovationCosts.currency(0))")
print("\nExpected Cash Flows:")
print(" Years 1-5: Annual rental income")
print(" Year 5: + Sale proceeds (\(saleProceeds.currency(0)))")
print(" Required Return: 12%")

// MARK: - Calculate NPV

// Define all cash flows
let cashFlows = [
-initialInvestment, // Year 0: Outflow
year1, // Year 1: Rental income
year2, // Year 2
year3, // Year 3
year4, // Year 4
year5 + saleProceeds // Year 5: Rental + sale
]

let requiredReturn = 0.12
let npvValue = npv(discountRate: requiredReturn, cashFlows: cashFlows)

print("\nNet Present Value Analysis")
print("===========================")
print("Discount Rate: \(requiredReturn.percent())")
print("NPV: \(npvValue.currency(0))")

if npvValue > 0 {
print("✓ Positive NPV - Investment adds value")
print(" For every $1 invested, you create \((1 + npvValue / initialInvestment).currency(2)) of value")
} else if npvValue < 0 {
print("✗ Negative NPV - Investment destroys value")
print(" Should reject this opportunity")
} else {
print("○ Zero NPV - Breakeven investment")
}

// MARK: - Calculate IRR

let irrValue = try irr(cashFlows: cashFlows)

print("\nInternal Rate of Return")
print("=======================")
print("IRR: \(irrValue.percent(2))")
print("Required Return: \(requiredReturn.percent())")

if irrValue > requiredReturn {
let spread = (irrValue - requiredReturn) * 100
print("✓ IRR exceeds required return by \(spread.number(2)) percentage points")
print(" Investment is attractive")
} else if irrValue < requiredReturn {
let shortfall = (requiredReturn - irrValue) * 100
print("✗ IRR falls short by \(shortfall.number(2)) percentage points")
} else {
print("○ IRR equals required return - Breakeven")
}

// Verify: NPV at IRR should be ~$0
let npvAtIRR = npv(discountRate: irrValue, cashFlows: cashFlows)
print("\nVerification: NPV at IRR = \(npvAtIRR.currency()) (should be ~$0)")


// MARK: - Additional Investment Metrics

// Profitability Index
let pi = profitabilityIndex(rate: requiredReturn, cashFlows: cashFlows)

print("\nProfitability Index")
print("===================")
print("PI: \(pi.number(2))")
if pi > 1.0 {
print("✓ PI > 1.0 - Creates value")
print(" Returns \(pi.currency(2)) for every $1 invested")
} else {
print("✗ PI < 1.0 - Destroys value")
}

// Payback Period
let payback = paybackPeriod(cashFlows: cashFlows)

print("\nPayback Period")
print("==============")
if let pb = payback {
print("Simple Payback: \(pb) years")
print(" Investment recovered in year \(pb)")
} else {
print("Investment never recovers initial outlay")
}

// Discounted Payback
let discountedPayback = discountedPaybackPeriod(
rate: requiredReturn,
cashFlows: cashFlows
)

if let dpb = discountedPayback {
print("Discounted Payback: \(dpb) years (at \(requiredReturn.percent()))")
if let pb = payback {
let difference = dpb - pb
print(" Takes \(difference) more years accounting for time value")
}
}

// MARK: - Sensitivity Analysis

print("\nSensitivity Analysis")
print("====================")

// Test different discount rates
print("NPV at Different Discount Rates:")
print("Rate | NPV | Decision")
print("------|------------|----------")

for rate in stride(from: 0.08, through: 0.16, by: 0.02) {
let npv = npv(discountRate: rate, cashFlows: cashFlows)
let decision = npv > 0 ? "Accept" : "Reject"
print("\(rate.percent(0).paddingLeft(toLength: 5)) | \(npv.currency(0).paddingLeft(toLength: 10)) | \(decision)")
}

// Test different sale prices
print("\nNPV at Different Sale Prices:")
print("Sale Price | Net Proceeds | NPV | Decision")
print("-----------|--------------|------------|----------")

for price in stride(from: 240_000.0, through: 340_000.0, by: 20_000.0) {
let proceeds = price - mortgagePayoff
let flows = [-initialInvestment, year1, year2, year3, year4, year5 + proceeds]
let npv = npv(discountRate: requiredReturn, cashFlows: flows)
let decision = npv > 0 ? "Accept" : "Reject"
print("\(price.currency(0).paddingLeft(toLength: 10)) | \(proceeds.currency(0).paddingLeft(toLength: 12)) | \(npv.currency(0).paddingLeft(toLength: 10)) | \(decision)")
}

// MARK: - Breakeven Analysis

print("\nBreakeven Analysis:")

var low = 200_000.0
var high = 350_000.0
var breakeven = (low + high) / 2

// Binary search for breakeven
for _ in 0..<20 {
let proceeds = breakeven - mortgagePayoff
let flows = [-initialInvestment, year1, year2, year3, year4, year5 + proceeds]
let npv = npv(discountRate: requiredReturn, cashFlows: flows)

if abs(npv) < 1.0 { break } // Close enough
else if npv > 0 { high = breakeven }
else { low = breakeven }

breakeven = (low + high) / 2
}

print("Breakeven Sale Price: \(breakeven.currency(0))")
print(" At this price, NPV = $0 and IRR = \(requiredReturn.percent())")
print(" Current assumption: \(salePrice.currency(0))")
print(" Safety margin: \((salePrice - breakeven).currency(0)) (\(((salePrice - breakeven) / salePrice).percent(1)))")

// MARK: - Compare Multiple Investments

print("\nComparing Investment Opportunities")
print("===================================")

struct Investment {
let name: String
let cashFlows: [Double]
let description: String
}

let investments = [
Investment(
name: "Real Estate",
cashFlows: [-70_000, 8_000, 8_500, 9_000, 9_500, 120_000],
description: "Rental property with 5-year hold"
),
Investment(
name: "Stock Portfolio",
cashFlows: [-70_000, 5_000, 5_500, 6_000, 6_500, 75_000],
description: "Diversified equity portfolio"
),
Investment(
name: "Business Expansion",
cashFlows: [-70_000, 0, 10_000, 15_000, 20_000, 40_000],
description: "Expand product line (delayed returns)"
)
]

print("\nInvestment | NPV | IRR | PI | Payback")
print("------------------|-----------|---------|------|--------")

var results: [(name: String, npv: Double, irr: Double)] = []

for investment in investments {
let npv = npv(discountRate: requiredReturn, cashFlows: investment.cashFlows)
let irr = try irr(cashFlows: investment.cashFlows)
let pi = profitabilityIndex(rate: requiredReturn, cashFlows: investment.cashFlows)
let pb = paybackPeriod(cashFlows: investment.cashFlows) ?? 99

results.append((investment.name, npv, irr))
print("\(investment.name.padding(toLength: 17, withPad: " ", startingAt: 0)) | \(npv.currency(0).paddingLeft(toLength: 9)) | \(irr.percent(1).paddingLeft(toLength: 7)) | \(pi.number(2)) | \(pb) yrs")
}

// Rank by NPV
let ranked = results.sorted { $0.npv > $1.npv }

print("\nRanking by NPV:")
for (i, result) in ranked.enumerated() {
print(" \(i + 1). \(result.name) - NPV: \(result.npv.currency(0))")
}

print("\nRecommendation: Choose '\(ranked[0].name)'")
print(" Highest NPV = Maximum value creation")

// MARK: - Irregular Cash Flow Analysis

print("\nIrregular Cash Flow Analysis")
print("============================")

let startDate = Date()
let dates = [
startDate, // Today: Initial investment
startDate.addingTimeInterval(90 * 86400), // 90 days
startDate.addingTimeInterval(250 * 86400), // 250 days
startDate.addingTimeInterval(400 * 86400), // 400 days
startDate.addingTimeInterval(600 * 86400), // 600 days
startDate.addingTimeInterval(5 * 365 * 86400) // 5 years
]

let irregularFlows = [-70_000.0, 8_000, 8_500, 9_000, 9_500, 120_000]

// XNPV accounts for exact dates
let xnpvValue = try xnpv(rate: requiredReturn, dates: dates, cashFlows: irregularFlows)
print("XNPV (irregular timing): \(xnpvValue.currency())")

// XIRR finds return with irregular dates
let xirrValue = try xirr(dates: dates, cashFlows: irregularFlows)
print("XIRR (irregular timing): \(xirrValue.percent(2))")

// Compare to regular IRR (assumes annual periods)
let regularIRR = try irr(cashFlows: irregularFlows)
print("\nComparison:")
print(" Regular IRR (annual periods): \(regularIRR.percent(2))")
print(" XIRR (actual dates): \(xirrValue.percent(2))")
print(" Difference: \(((xirrValue - regularIRR) * 10000).number(0)) basis points")

→ Full API Reference: [**BusinessMath Docs – 3.8 Investment Analysis**](https://github.com/jpurnell/BusinessMath/blob/main/Sources/BusinessMath/BusinessMath.docc/3.8-InvestmentAnalysis.md) Modifications to try:
  1. Model your company’s capital project pipeline
  2. Compare equipment purchase vs. lease
  3. Calculate risk-adjusted NPV using CAPM
  4. Build Monte Carlo simulation around key assumptions

Real-World Application

Every CFO, investor, and analyst uses NPV/IRR daily: PE firm use case: “We have 15 potential acquisitions. Rank them by NPV at our 15% hurdle rate. Show sensitivity to exit multiple (6x, 8x, 10x EBITDA).”

BusinessMath makes this analysis programmatic, reproducible, and portfolio-wide.


★ Insight ─────────────────────────────────────

Why NPV is Superior to IRR for Decision-Making

IRR is intuitive (“this investment returns 23%!”) but has flaws:

Problem 1: Scale blindness

Problem 2: Multiple IRRs Problem 3: Reinvestment assumption The rule: Use NPV for decisions (maximizes value), IRR for communication (easy to understand).

─────────────────────────────────────────────────


📝 Development Note

The hardest implementation challenge was IRR convergence. IRR is calculated using Newton-Raphson iteration, which can fail if: We implemented robust error handling with:
  1. Bisection fallback if Newton-Raphson diverges
  2. Detection of multiple IRRs (warn user)
  3. Clear error messages when no IRR exists
Related Methodology: Test-First Development (Week 1) - We wrote tests for pathological cases (no IRR, multiple IRRs, near-zero cash flows) before implementing.

Next Steps

Coming up tomorrow: Equity Valuation - Pricing stocks using dividend discount models, FCFE, and residual income.

Thursday: Bond Valuation - Pricing bonds, credit spreads, and callable securities.


Series Progress:


Tagged with: investment-analysis