Skip to content

Cursor Task — Track A: sd-math Python Library

Section titled “Cursor Task — Track A: sd-math Python Library”

Model: claude-sonnet-4-6 (Sonnet is appropriate; this is deterministic port work, not strategic reasoning) Scope: Interest-only leveraged analysis only (Phase 1) Complete when: All 4 golden fixtures pass in pytest with no failures


You are porting the LevPro VB6 leveraged investing software to a Python package called sd-math. LevPro is a 20-year-old Windows application by Talbot Stevens (Financial Success Strategies) used by Canadian financial advisors to analyze investment loan strategies.

Your job is Track A: implement the Python math library and test suite. The SvelteKit front end (Track B) will consume the output of this library. You are building the calculation engine only — no API, no web server, no front end.


1. Design spec — read entirely before writing any code

Section titled “1. Design spec — read entirely before writing any code”
D:\FSS\KB\Business\_WorkingOn\Projects\SD-App\sd-math-design.md

WSL: /mnt/d/FSS/KB/Business/_WorkingOn/Projects/SD-App/sd-math-design.md

This document contains:

  • The full algorithm in 5 steps with exact formulas
  • All Python input/output model definitions with VB6 names annotated
  • VB6 constants to preserve
  • Testing requirements

2. VB6 source — the authoritative math spec

Section titled “2. VB6 source — the authoritative math spec”
packages/sd-math/LevPro/modTypes.bas ← all type definitions
packages/sd-math/LevPro/modMath.bas ← all math functions (PRIMARY)
packages/sd-math/LevPro/modLeveragePro.bas ← reference only if needed

3. Golden test fixtures — what your implementation must produce

Section titled “3. Golden test fixtures — what your implementation must produce”
packages/sd-math/tests/fixtures/interest-only-ca.json
packages/sd-math/tests/fixtures/LevPro-Test1.pdf ← one-page summary, 35% tax
packages/sd-math/tests/fixtures/LevPro-Test2.pdf ← one-page summary, 50% tax
packages/sd-math/tests/fixtures/LevPro-Test3.pdf ← 10-year projection, 35% tax, 10% return
packages/sd-math/tests/fixtures/LevPro-Test4.pdf ← 10-year projection, 50% tax, 7% return

  1. All math comes from VB6 source code. Only from VB6 source code.

    • The PDFs define what to produce, not how to calculate
    • Never infer formulas from PDF outputs
    • If the VB6 source is unclear, stop and ask
  2. TDD: write failing tests first, then implement to make them pass.

    • Load interest-only-ca.json and write parametrized pytest assertions before any implementation code
  3. Never report complete until all 4 golden fixtures pass in pytest.

  4. TypeScript never does math. This is a Python-only library. No JavaScript/TypeScript.


packages/sd-math/
├── pyproject.toml ← Python package config (see below)
├── src/
│ └── sd_math/
│ ├── __init__.py ← public entry point: calculate()
│ ├── models/
│ │ ├── inputs.py ← RegionProfile, TaxProfile, ReturnProfile, LoanProfile, UserProfile
│ │ └── outputs.py ← AnnualRow, SummaryScenario, ProjectionSummary, ProjectionResult
│ ├── tax/
│ │ ├── canada.py ← Canadian tax helpers (dividend gross-up, QC deductibility)
│ │ └── us.py ← stub: raise NotImplementedError
│ └── strategies/
│ ├── interest_only.py ← Port of CalcIntOnlyLevAnalysis (Phase 1 — implement fully)
│ ├── term_loan.py ← stub: raise NotImplementedError
│ └── rrsp.py ← stub: raise NotImplementedError
└── tests/
├── fixtures/ ← already exists with PDFs and JSON
├── conftest.py ← load fixture JSON, define shared helpers
└── test_interest_only.py ← parametrized golden fixture tests

pyproject.toml (use this as starting point)

Section titled “pyproject.toml (use this as starting point)”
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "sd-math"
version = "0.1.0"
description = "SD App math library — LevPro VB6 port"
requires-python = ">=3.11"
dependencies = []
[project.optional-dependencies]
dev = ["pytest", "pytest-approx"]
[tool.hatch.build.targets.wheel]
packages = ["src/sd_math"]

Input Model Summary (from sd-math-design.md)

Section titled “Input Model Summary (from sd-math-design.md)”

All Python field names with VB6 counterparts:

Python fieldVB6 nameNotes
marginal_rateTaxRateCombined = prov + fed
prov_tax_rateProvTaxRateProvincial portion
fed_tax_rateFedTaxRateFederal portion
cg_inclusion_rateCapGainsInclusionRateCanada: 0.50
div_tax_rateDivTaxRateExplicit input (e.g. 0.159 at 35%)
expected_returnReturnPerPeriodAnnual total before-tax rate
dcg_pctDCGRatioOfReturnDeferred CG portion (e.g. 0.70)
taxable_cg_pctCapGainsRatioOfReturnAnnually-realized CG (e.g. 0.25)
div_pctDivRatioOfReturnDividend portion (e.g. 0.05)
interest_pctIntRatioOfReturnInterest portion (typically 0.00)
distrib_reinvest_ratioDistribReinvestRatioAlways 1.0
loan_amountLoanAmounte.g. 100000
interest_rateLoanInterestRateAnnual before-tax rate (e.g. 0.09)
holding_periodNPeriodsYears (e.g. 10)
lev_to_non_lev_ratioLevToNonLevRatioAlways 0.0 for pure leveraged analysis

RegionProfile fields (from IntOnlyLoanInputsType):

Python fieldVB6 nameONQC
prov_deductibility_limitedProvDeductibilityLimitedFalseTrue
fed_deductibility_limitedFedDeductibilityLimitedFalseFalse
prov_ratio_of_interest_deductibleProvRatioOfInterestDeductible1.01.0
fed_ratio_of_interest_deductibleFedRatioOfInterestDeductible1.01.0
prov_cap_gains_are_incomeProvCapGainsAreIncomeFalseTrue
fed_cap_gains_are_incomeFedCapGainsAreIncomeFalseFalse
prov_defer_unused_deductionsProvDeferUnusedDeductionsFalseTrue
fed_defer_unused_deductionsFedDeferUnusedDeductionsFalseFalse

Python fieldVB6 sourceDescription
no_lev_bt_balanceIntOnlyNoLevGenericOutputs.BTBalance[t]Unlev portfolio market value
no_lev_at_distribIntOnlyNoLevGenericOutputs.ATDistrib[t]Unlev after-tax distributions
no_lev_acbIntOnlyNoLevGenericOutputs.ACB[t]Unlev adjusted cost base
lev_bt_balanceIntOnlyLevGenericOutputs.BTBalance[t]Gross leveraged portfolio value
lev_at_distribIntOnlyLevGenericOutputs.ATDistrib[t]Lev after-tax distributions
net_lev_bt_balIntOnlyLoanOutputs.NetLevBTBal[t]Net value after repaying loan
net_lev_acbIntOnlyLoanOutputs.NetLevACB[t]Net ACB after repaying loan
net_increasecomputednet_lev_bt_bal - no_lev_bt_balance
pct_increasecomputednet_increase / no_lev_bt_balance

Summary outputs:

Python fieldDescription
better_than_returnIntOnlyBetterThan() bisection result
at_annual_investmentGenericInputs.Cashflow[1] (AT annual cost)
bt_annual_interestLoanAmount × LoanInterestRate

All 4 fixtures use: $100,000 loan, 9% interest, 10-year horizon, 70% deferred CG / 25% taxable CG / 5% dividends, Ontario (ON), lev_to_non_lev_ratio=0.0, distrib_reinvest_ratio=1.0.

Fixture IDmarginal_ratediv_tax_rateReturnAssertions
io-ca-35pct-summary0.350.1590%, 3%, 6.7%, 7%, 10%5 summary scenarios
io-ca-50pct-summary0.500.3130%, 3%, 5.6%, 7%, 10%5 summary scenarios
io-ca-35pct-10pct-projection0.350.15910%10 annual rows + summary
io-ca-50pct-07pct-projection0.500.3137%10 annual rows + summary

Use pytest.approx(value, abs=1) for dollar amounts (nearest dollar), pytest.approx(value, abs=0.01) for percentages and rates.

Missing from fixture (derive these):

  • prov_tax_rate and fed_tax_rate are not in the fixture; use standard Canadian split: at 35% marginal → prov ≈ 0.1315, fed ≈ 0.2185 (the exact split doesn’t affect non-QC results since deductibility is unlimited — but both must be provided). Ask if uncertain.

The full step-by-step algorithm is in sd-math-design.md. Brief summary:

  1. CalcGenericFutureValues — share-price model; price grows by deferred-CG portion only; distributions = non-deferred portion of return; ACB grows by AT distributions + cashflows
  2. CalcIntOnlyLevAnalysis — sets up leveraged initial (LoanAmount, no periodic investments), runs CalcGenericFutureValues, calculates annual AT cashflow, calls CalcNetLevValues per year, then runs unlev comparison (same AT cashflow invested annually at end of period)
  3. CalcNetLevValues — shares model to calculate net balance after selling to repay loan: LoanTax = LoanTaxableIncome × TaxRate / (1 - (1 - ACPS) × CapGainsTaxRate)
  4. IntOnlyBetterThan — bisection: lower=0%, upper=LoanInterestRate×1.2 expanding by 1.2× until positive; convergence at width < 0.000005; verify |result| < 0.0005 × original_magnitude
  5. Display artifact — at “Better Than” return, both lev and unlev end-balances shown as their average

  1. Set up pyproject.toml and package skeleton
  2. Write models/inputs.py and models/outputs.py
  3. Write tests/conftest.py — load interest-only-ca.json
  4. Write tests/test_interest_only.py — parametrized assertions against all 4 fixtures (all fail initially)
  5. Implement strategies/interest_only.pyCalcGenericFutureValues first (simplest inner loop)
  6. Add CalcNetLevValues
  7. Add CalcIntOnlyLevAnalysis + unlev comparison
  8. Add IntOnlyBetterThan bisection
  9. Run pytest — iterate until all 4 fixtures pass

  • Term loan analysis (CalcTermLevAnalysis) — stub only, no fixtures available yet
  • RRSP analysis — stub only
  • FastAPI server
  • Snapshot generator CLI
  • Any front-end code

Created 2026-03-22. Scope: Phase 1 interest-only. Target: 4 golden fixtures passing.



2026-03-22

37/37 pytest tests pass across all 4 golden fixtures.

Package structure (packages/sd-math/):

  • pyproject.toml — Hatchling build, sd-math package, pytest dev dependency

  • src/sd_math/init.py — public calculate() dispatcher

  • src/sd_math/models/inputs.py — RegionProfile, TaxProfile, ReturnProfile, LoanProfile, UserProfile

  • src/sd_math/models/outputs.py — AnnualRow, SummaryScenario, ProjectionSummary, ProjectionResult

  • src/sd_math/tax/canada.py — constants; us.py — stub

  • src/sd_math/strategies/interest_only.py — full VB6 port of all 5 algorithm steps

  • src/sd_math/strategies/term_loan.py, rrsp.py — stubs

VB6 functions ported (all verbatim from modMath.bas / modLeveragePro.bas):

  1. CalcGenericFutureValues — share-price model, positive/negative return branches

  2. CalcIntOnlyLevAnalysis — leveraged pass, AT cashflow, no-leverage pass

  3. CalcNetLevValues — net balance after loan repayment (full QC deduction branching included)

  4. IntOnlyBetterThan — bisection convergence to MAX_RETURN_ERROR = 0.000005

  5. Display artifact — better_than row shows average of lev/no-lev values

Test coverage: 37 assertions across all 4 fixture IDs — 4 summary scenarios (×2 tax rates), 2 better_than bisection results, 20 annual row projections (10 years × 2 scenarios), and derived values (at_annual_investment, bt_annual_interest).