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
Context
Section titled “Context”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.
Files to Read (in this order)
Section titled “Files to Read (in this order)”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.mdWSL: /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 definitionspackages/sd-math/LevPro/modMath.bas ← all math functions (PRIMARY)packages/sd-math/LevPro/modLeveragePro.bas ← reference only if needed3. 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.jsonpackages/sd-math/tests/fixtures/LevPro-Test1.pdf ← one-page summary, 35% taxpackages/sd-math/tests/fixtures/LevPro-Test2.pdf ← one-page summary, 50% taxpackages/sd-math/tests/fixtures/LevPro-Test3.pdf ← 10-year projection, 35% tax, 10% returnpackages/sd-math/tests/fixtures/LevPro-Test4.pdf ← 10-year projection, 50% tax, 7% returnHard Rules
Section titled “Hard Rules”-
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
-
TDD: write failing tests first, then implement to make them pass.
- Load
interest-only-ca.jsonand write parametrized pytest assertions before any implementation code
- Load
-
Never report complete until all 4 golden fixtures pass in pytest.
-
TypeScript never does math. This is a Python-only library. No JavaScript/TypeScript.
Deliverables
Section titled “Deliverables”Package structure to create
Section titled “Package structure to create”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 testspyproject.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 field | VB6 name | Notes |
|---|---|---|
marginal_rate | TaxRate | Combined = prov + fed |
prov_tax_rate | ProvTaxRate | Provincial portion |
fed_tax_rate | FedTaxRate | Federal portion |
cg_inclusion_rate | CapGainsInclusionRate | Canada: 0.50 |
div_tax_rate | DivTaxRate | Explicit input (e.g. 0.159 at 35%) |
expected_return | ReturnPerPeriod | Annual total before-tax rate |
dcg_pct | DCGRatioOfReturn | Deferred CG portion (e.g. 0.70) |
taxable_cg_pct | CapGainsRatioOfReturn | Annually-realized CG (e.g. 0.25) |
div_pct | DivRatioOfReturn | Dividend portion (e.g. 0.05) |
interest_pct | IntRatioOfReturn | Interest portion (typically 0.00) |
distrib_reinvest_ratio | DistribReinvestRatio | Always 1.0 |
loan_amount | LoanAmount | e.g. 100000 |
interest_rate | LoanInterestRate | Annual before-tax rate (e.g. 0.09) |
holding_period | NPeriods | Years (e.g. 10) |
lev_to_non_lev_ratio | LevToNonLevRatio | Always 0.0 for pure leveraged analysis |
RegionProfile fields (from IntOnlyLoanInputsType):
| Python field | VB6 name | ON | QC |
|---|---|---|---|
prov_deductibility_limited | ProvDeductibilityLimited | False | True |
fed_deductibility_limited | FedDeductibilityLimited | False | False |
prov_ratio_of_interest_deductible | ProvRatioOfInterestDeductible | 1.0 | 1.0 |
fed_ratio_of_interest_deductible | FedRatioOfInterestDeductible | 1.0 | 1.0 |
prov_cap_gains_are_income | ProvCapGainsAreIncome | False | True |
fed_cap_gains_are_income | FedCapGainsAreIncome | False | False |
prov_defer_unused_deductions | ProvDeferUnusedDeductions | False | True |
fed_defer_unused_deductions | FedDeferUnusedDeductions | False | False |
Output Model Summary
Section titled “Output Model Summary”| Python field | VB6 source | Description |
|---|---|---|
no_lev_bt_balance | IntOnlyNoLevGenericOutputs.BTBalance[t] | Unlev portfolio market value |
no_lev_at_distrib | IntOnlyNoLevGenericOutputs.ATDistrib[t] | Unlev after-tax distributions |
no_lev_acb | IntOnlyNoLevGenericOutputs.ACB[t] | Unlev adjusted cost base |
lev_bt_balance | IntOnlyLevGenericOutputs.BTBalance[t] | Gross leveraged portfolio value |
lev_at_distrib | IntOnlyLevGenericOutputs.ATDistrib[t] | Lev after-tax distributions |
net_lev_bt_bal | IntOnlyLoanOutputs.NetLevBTBal[t] | Net value after repaying loan |
net_lev_acb | IntOnlyLoanOutputs.NetLevACB[t] | Net ACB after repaying loan |
net_increase | computed | net_lev_bt_bal - no_lev_bt_balance |
pct_increase | computed | net_increase / no_lev_bt_balance |
Summary outputs:
| Python field | Description |
|---|---|
better_than_return | IntOnlyBetterThan() bisection result |
at_annual_investment | GenericInputs.Cashflow[1] (AT annual cost) |
bt_annual_interest | LoanAmount × LoanInterestRate |
Golden Fixture Test Parameters
Section titled “Golden Fixture Test Parameters”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 ID | marginal_rate | div_tax_rate | Return | Assertions |
|---|---|---|---|---|
io-ca-35pct-summary | 0.35 | 0.159 | 0%, 3%, 6.7%, 7%, 10% | 5 summary scenarios |
io-ca-50pct-summary | 0.50 | 0.313 | 0%, 3%, 5.6%, 7%, 10% | 5 summary scenarios |
io-ca-35pct-10pct-projection | 0.35 | 0.159 | 10% | 10 annual rows + summary |
io-ca-50pct-07pct-projection | 0.50 | 0.313 | 7% | 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_rateandfed_tax_rateare 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.
Algorithm Reference
Section titled “Algorithm Reference”The full step-by-step algorithm is in sd-math-design.md. Brief summary:
CalcGenericFutureValues— share-price model; price grows by deferred-CG portion only; distributions = non-deferred portion of return; ACB grows by AT distributions + cashflowsCalcIntOnlyLevAnalysis— sets up leveraged initial (LoanAmount, no periodic investments), runsCalcGenericFutureValues, calculates annual AT cashflow, callsCalcNetLevValuesper year, then runs unlev comparison (same AT cashflow invested annually at end of period)CalcNetLevValues— shares model to calculate net balance after selling to repay loan:LoanTax = LoanTaxableIncome × TaxRate / (1 - (1 - ACPS) × CapGainsTaxRate)IntOnlyBetterThan— bisection: lower=0%, upper=LoanInterestRate×1.2expanding by 1.2× until positive; convergence at width < 0.000005; verify|result| < 0.0005 × original_magnitude- Display artifact — at “Better Than” return, both lev and unlev end-balances shown as their average
Suggested Implementation Order
Section titled “Suggested Implementation Order”- Set up
pyproject.tomland package skeleton - Write
models/inputs.pyandmodels/outputs.py - Write
tests/conftest.py— loadinterest-only-ca.json - Write
tests/test_interest_only.py— parametrized assertions against all 4 fixtures (all fail initially) - Implement
strategies/interest_only.py—CalcGenericFutureValuesfirst (simplest inner loop) - Add
CalcNetLevValues - Add
CalcIntOnlyLevAnalysis+ unlev comparison - Add
IntOnlyBetterThanbisection - Run pytest — iterate until all 4 fixtures pass
Out of Scope (do not implement)
Section titled “Out of Scope (do not implement)”- 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.
Complete: sd-math Phase 1
Section titled “Complete: sd-math Phase 1”2026-03-22
37/37 pytest tests pass across all 4 golden fixtures.
What was implemented
Section titled “What was implemented”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):
-
CalcGenericFutureValues — share-price model, positive/negative return branches
-
CalcIntOnlyLevAnalysis — leveraged pass, AT cashflow, no-leverage pass
-
CalcNetLevValues — net balance after loan repayment (full QC deduction branching included)
-
IntOnlyBetterThan — bisection convergence to MAX_RETURN_ERROR = 0.000005
-
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).