Get up and running with Per%Sense in 5 minutes.
For a per-screen field-by-field reference, see the full Help document.
go version)github.com/shopspring/decimal)No database, no external services, no build step for the frontend. The HTML/CSS/JS is embedded in the Go binary via go:embed.
From the project root:
# Quick run (no compile artifact) ./start_server.sh # or equivalently: go run ./cmd/persense # Compiled binary go build -o persense ./cmd/persense ./persense # default port 8080 ./persense -port 9090 # custom port
Open http://localhost:8080 in a browser.
The home screen offers three calculators. Each accepts inputs as hard data (your numbers) or returns soft data (computed answers).
Compare loan options, compute payments, and evaluate points-vs-rate trade-offs. Fill in any consistent combination of Price, % Down (or Cash or Amt Borrowed), Rate, Years, Tax, optional Points and Balloon. The Calc engine solves for whichever of Price, Monthly, or Balloon Amount is left blank. APR is computed when there's enough data. Use Compare APR to find the crossover term between two rows.
Detailed reference: Help → Mortgage Screen
Generate a full payment schedule. Required: Loan Amount, Loan Date, Rate %, Pmts/Yr, and either # Periods or Last Pmt Date. Optional: 1st Pmt Date (defaults to Loan Date + 1 period if omitted), Payment (leave blank and the system computes it exactly via the annuity formula).
The Amortization screen supports field-presence dispatch on the top row: omit one of First Pmt Date, Last Pmt Date, # Periods, or Payment and Per%Sense fills it in.
The Advanced Options panel adds:
| Option | What it does |
|---|---|
| Prepayments | Extra periodic payments between two dates at a given frequency |
| Balloons | One-time lump payments on specific dates |
| Adjustments | Rate and/or payment changes on specific dates (ARMs) |
| Moratorium | Interest-only deferment until a given date |
| Target | Minimum principal reduction per payment (payment adjusts upward if needed) |
| Skip Months | Months with zero payment, e.g. 6-8,12 skips Jun–Aug and Dec |
Filling any Advanced field automatically switches the engine into fancy mode.
Target and Skip Months are set, the target's minimum-principal-reduction overrides the skip-month zeroing. This matches the original DOS code.
Validation: Per%Sense rejects inconsistent inputs
before computing a schedule. Common rejections: balloon date before 1st
Pmt Date; two rate adjustments on the same day; adjustment date
on/before the Loan Date or on/after the Last Pmt Date; moratorium
first-repay before 1st Pmt Date; target principal reduction larger than
Amount / # Periods. The full list lives on
the Help page.
Detailed reference: Help → Amortization Screen
Discounted-cash-flow analysis. Two grids:
Plus the top-level controls: As-of Date, Rate Type, Rate %, Present Value (read-only output).
Backward solve — leave one field blank and provide a target Present Value:
| Leave blank | The system solves for… |
|---|---|
Rate | The IRR (internal rate of return) |
As-of Date | The date that produces the target PV |
| Lump-sum amount | The missing payment value |
| Lump-sum date | When the payment occurs |
| Periodic amount, fromDate, or toDate | That field |
Life contingency and variable-rate schedules are exposed as collapsible sections.
Detailed reference: Help → Present Value Screen
Five POST endpoints, all accepting and returning JSON.
| Endpoint | Purpose |
|---|---|
POST /api/mortgage/calc | Mortgage row Calc with optional APR |
POST /api/mortgage/compare | Compare two mortgages' APRs, including crossover time |
POST /api/mortgage/whatif | Generate a what-if table by stepping one field |
POST /api/amortization/calc | Schedule generation; supports Advanced Options and backward solve |
POST /api/presentvalue/calc | Forward PV or backward solve |
GET /api/health | {"status":"ok"} |
Optional fields are JSON pointers: omit them entirely (do not send null) to indicate "blank".
curl -s -X POST http://localhost:8080/api/mortgage/calc \
-H 'Content-Type: application/json' \
-d '{
"price": 200000,
"pctDown": 0.20,
"years": 30,
"rate": 0.06,
"points": 0
}' | jq
Response includes monthly, cash, financed, and apr if there's enough data.
Send two mortgages as a and b. The response gives each full-term APR and, when the APRs cross, the crossover APR and the holding period beyond which the other loan becomes cheaper.
curl -s -X POST http://localhost:8080/api/mortgage/compare \
-H 'Content-Type: application/json' \
-d '{
"a": {"price":200000,"pctDown":0.20,"years":30,"rate":0.06,"points":0},
"b": {"price":200000,"pctDown":0.20,"years":30,"rate":0.0575,"points":0.02}
}' | jq
Response fields: apr1, apr2, crossoverApr, crossoverYears, summary.
Step one field of a base mortgage across count rows. vary is one of rate, years, points, pctDown, price, or monthly; increment is the per-row step.
curl -s -X POST http://localhost:8080/api/mortgage/whatif \
-H 'Content-Type: application/json' \
-d '{
"base": {"price":200000,"pctDown":0.20,"years":30,"rate":0.06,"points":0},
"vary": "rate",
"increment": 0.0025,
"count": 5
}' | jq
Response is rows[], each a fully-computed mortgage line (row 1 is the base; each subsequent row steps vary by increment).
payment)Send the loan parameters and leave payment at zero (or omit it). The handler treats a zero payment as "blank" and the engine computes it via the annuity formula:
curl -s -X POST http://localhost:8080/api/amortization/calc \
-H 'Content-Type: application/json' \
-d '{
"amount": 250000,
"loanDate": "2024-01-01",
"rate": 0.06,
"perYr": 12,
"nPeriods": 360
}' | jq
Note that firstDate is also omitted — the engine defaults it to 2024-02-01 (loanDate + 1 month). The response's first schedule row will show the computed Payment of ~$1,498.88.
amount)Omit amount and supply rate and payment; the handler dispatches to the loan-amount back-solver. (Omitting both amount and rate is instead the term-derivation shortcut, so supply at least the rate here.)
curl -s -X POST http://localhost:8080/api/amortization/calc \
-H 'Content-Type: application/json' \
-d '{
"rate": 0.06,
"loanDate": "2024-01-01",
"perYr": 12,
"nPeriods": 360,
"payment": 1498.88
}' | jq
The response's amount field carries the solved principal — here ≈ 250000.62 (the cents are an artifact of the payment being rounded to whole cents) — alongside the full schedule.
rate)Omit rate and supply amount and payment; the handler dispatches to the rate back-solver (Newton iteration on the schedule residual). The term may be given as nPeriods or as lastDate — the handler derives the period count first when needed.
curl -s -X POST http://localhost:8080/api/amortization/calc \
-H 'Content-Type: application/json' \
-d '{
"amount": 250000,
"loanDate": "2024-01-01",
"perYr": 12,
"nPeriods": 360,
"payment": 1498.88
}' | jq
The response's rate field carries the solved rate — here ≈ 0.06 — alongside the full schedule.
curl -s -X POST http://localhost:8080/api/amortization/calc \
-H 'Content-Type: application/json' \
-d '{
"amount": 200000,
"loanDate": "2024-01-01",
"firstDate": "2024-02-01",
"rate": 0.06,
"perYr": 12,
"nPeriods": 360,
"payment": 1199.10,
"prepayments": [
{"startDate":"2024-02-01","stopDate":"2029-02-01","perYr":12,"amount":100}
],
"balloons": [{"date":"2030-01-01","amount":50000}],
"adjustments": [{"date":"2027-01-01","rate":0.05}],
"moratorium": "2025-01-01",
"targetAmt": 500,
"skipMonths": "12"
}' | jq
Returns schedule[] (one row per payment), totalPaid, totalInterest.
Forward (compute PV from inputs):
curl -s -X POST http://localhost:8080/api/presentvalue/calc \
-H 'Content-Type: application/json' \
-d '{
"asOfDate": "2024-01-01",
"rate": 0.06,
"lumpSums": [
{"date":"2025-01-01","amount":10000},
{"date":"2026-01-01","amount":10000}
],
"periodics": [
{"fromDate":"2024-02-01","toDate":"2034-01-01","perYr":12,"amount":500}
]
}' | jq
Solve for the unknown rate — omit rate, supply sumValue:
curl -s -X POST http://localhost:8080/api/presentvalue/calc \
-H 'Content-Type: application/json' \
-d '{
"asOfDate": "2024-01-01",
"sumValue": 18334.71,
"lumpSums": [{"date":"2026-01-01","amount":20000}]
}' | jq
Solve for a missing lump-sum amount — omit amount on the row, supply sumValue:
curl -s -X POST http://localhost:8080/api/presentvalue/calc \
-H 'Content-Type: application/json' \
-d '{
"asOfDate": "2024-01-01",
"rate": 0.06,
"sumValue": 9433.96,
"lumpSums": [{"date":"2025-01-01"}]
}' | jq
The response shape is the same as forward calc; the previously-blank field will be populated.
# Whole suite go test ./... # One package, verbose go test ./internal/finance/presentvalue/ -v # Single test go test ./internal/finance/presentvalue/ -run TestRoundTripRate -v # DOS reference-data cross-checks only go test ./internal/finance/ -run TestCrossCheck -v
The DOS reference values used by the cross-checks live at legacy/reference-output/refdata.json. They were generated by legacy/testharness/refdata.pas under Free Pascal. When adding new financial functions, run the harness and append new entries; do not hand-edit the JSON.
| File | Coverage |
|---|---|
internal/finance/crosscheck_test.go | DOS forward regression cases |
internal/finance/crosscheck_backward_test.go | DOS regression for backward solvers |
presentvalue/backward_test.go | BackwardCalc round-trip + FirstPass classification |
presentvalue/backward_boundary_test.go | Threshold/edge cases (cola=rate, near-teeny rate, etc.) |
amortization/backward_test.go | SolveLoanAmount, SolveRate |
amortization/advanced_test.go | Each Advanced Option in isolation |
mortgage/rowgen_test.go | Row generation |
api/pv_backward_test.go | Backward PV via HTTP |
api/amort_advanced_test.go | Advanced Options via HTTP |
persense-port/ ├── CLAUDE.md ← read first; project conventions and ported-status ├── go.mod ├── start_server.sh ← one-line `go run` shortcut ├── cmd/persense/ │ ├── main.go ← HTTP server, /api/* routes, embeds static │ └── static/ │ ├── index.html ← single-page UI (Tailwind CDN + vanilla JS) │ ├── help.html ← per-screen field reference │ └── quickstart.html ← (this page) ├── internal/ │ ├── api/ ← HTTP handlers │ ├── dateutil/ ← Date arithmetic ported from INTSUTIL.pas │ ├── fileio/ ← Legacy file I/O │ ├── finance/ ← All financial logic │ │ ├── actuarial/ │ │ ├── amortization/ ← engine.go, backward.go, types.go │ │ ├── interest/ ← Exxp, Lnn, Round2, RateFromYield, … │ │ ├── mortgage/ ← mortgage.go, rowgen.go │ │ └── presentvalue/ ← calc.go, backward.go, types.go │ └── types/ ← DateRec, status enums, constants ├── docs/ │ ├── requirements.md ← Worksheet specs │ ├── discrepancies.md ← Known DOS↔port behavioral differences │ ├── missing_flows.md ← Field-presence dispatch audit │ └── QUICKSTART.md ← Markdown source of this page ├── legacy/ │ ├── src/dos_source/ ← Original DOS Pascal — READ-ONLY, financial authority │ ├── src/win_source/ ← Windows Pascal port — UI/help authority │ └── reference-output/refdata.json ← DOS-known-good test values └── pkg/ ← (currently empty)
legacy/src/dos_source/internal/finance/<area>/ package// Ported from legacy/src/dos_source/<File>.pas: <function> comment_test.go with at least: one round-trip test, one DOS-regression test driven from refdata.json, and threshold/boundary cases per the patterns in backward_boundary_test.golegacy/testharness/refdata.pas, regenerate refdata.json, and update the refData struct in internal/finance/crosscheck_test.gointernal/api/handlers.gonil → StatusEmpty, present → InOutInput in the handlerhandlers.go)internal/api/*_test.gocmd/persense/static/index.html and update getAmzInput / getMortgageInput / getPVInput in the embedded JSrefdata.json for the input shape, or run the original program if availablecrosscheck_test.gobackward_test.goMortgage.pas — mortgage Calc, APR comparison, row generationPRESVALU.pas — present value forward + backward calcAmortize.pas + AMORTOP.pas — amortization Calc + RepayFancyLoan engineINTSUTIL.pas — NumberOfInstallments, AddPeriod, basic math