Skip to content

go-yfinance v1.6.0 Development Progress

Last Updated: 2026-08-17

Overview

Implementation plan and progress tracking for Python yfinance v1.6.0 parity.

Python yfinance v1.6.0 is a feature release. The bulk of the change is a large price-repair overhaul in yfinance/scrapers/history.py (~708 changed lines): volume-based cross-checking of unit-switch/split detection, DBSCAN-based calibration pruning, a rewritten dividend-adjust repair for intraday data, and a user-visible fix so repair=True no longer permanently converts GBp/ZAc/ILA prices to the main currency (upstream #2907/#2943/#2908). Outside repair, the release adds two screener fields and four balance-sheet keys, improves error messages (surface Yahoo's reason instead of claiming "possibly delisted"), fixes a Lookup error-handling bug, guards _fetch_info against a JSON-null result, and migrates packaging to pyproject.toml.

go-yfinance's repair package is a re-implementation, not a line-by-line port: it repairs bars in place with statistical fills and has no sub-interval reconstruction (_reconstruct_intervals_batch has no Go analogue — verified: pkg/repair/ contains no re-fetch path). Upstream items that live inside the reconstruction machinery are therefore N/A; items that change detection thresholds, volume cross-checks, currency handling, and data tables port directly onto the existing Go functions.

The purpose of this document is to record the analysis before any code change, so each upstream item can be tracked as implemented, adapted, intentionally skipped, or verified as not applicable to Go.

Branch Structure

main
 └── feature/v1.6.0-parity

Upstream Release Scope

Commits in 1.5.2..1.6.0 (python-yfinance, beac22d..0af231f), key items:

0af231f Version 1.6.0
8b85f90 Price repair improvements (#2943)
f854309 Fix regression in PR #2907 (see last comment)
880728a Fix: repair=True should not permanently convert GBp/ZAc/ILA prices (#2907)
bb11309 Price repair fixes & improvements (#2908)
9bde808 fix: handle null result from Yahoo API in _fetch_info (#2906)
fb6af9d Surface Yahoo's reason directly instead of wrapping it
9d672c6 Don't claim 'possibly delisted' when Yahoo explains the missing data (#2903)
57b9482 Fix error messages showing internal 15m interval for 30m requests (#2900)
8676e8d Fix AttributeError in Lookup error handling (#2896)
59e6a9c fix: handle read-only Adj Close array in div-adjust repair (#2897)
33b12b6 Add missing balance sheet keys (#2879)
bf8ff78 fix: add dividendyield screener fields (#2888)
d8e2cfe / 9a5b72a / f3ef5f4  timedelta/numpy deprecation fixes (#2891, #2915)
a166d01 / a18927b  Migrate packaging to pyproject.toml (#2920, #2849)
f8f2f87 add scikit-learn as a repair dependency
c9bc470 Pin ruff ruleset for deterministic CI (#2919)
24ee2ff docs: state download()'s returned index timezone contract (#2936)

File-level diff (yfinance sources only):

 yfinance/const.py            |   4 +-
 yfinance/exceptions.py       |  26 +-
 yfinance/lookup.py           |   4 +-
 yfinance/multi.py            |   5 +   (docstring only)
 yfinance/scrapers/history.py | 708 +++++++++----------
 yfinance/scrapers/quote.py   |   6 +-
 yfinance/utils.py            |  43 +-
 tests/data/ASAI-L-1h-bad-unit*.csv (new fixtures)

Upstream Item Assessment

# Upstream Item Python Change Go Assessment
1 #2888 screener fields EQUITY_SCREENER_FIELDS["profitability"] += "dividendyield", "dividendpershare.lasttwelvemonths" Port. Add both strings to EquityScreenerFields["profitability"] in pkg/models/screener_const.go.
2 #2879 balance-sheet keys fundamentals_keys['balance-sheet'] += FixedMaturityInvestments, EquityInvestments, NetLoan, DeferredAssets (after CashCashEquivalentsAndFederalFundsSold) Port. Add all four to BalanceSheetKeys in internal/endpoints/endpoints.go.
3 #2903 + follow-up: surface Yahoo's reason, drop "possibly delisted" YFPricesMissingError(..., yahoo_reason=...): when Yahoo returns a chart error, its description becomes the whole message Mostly aligned already. Go never had "possibly delisted" wording; pkg/ticker/history.go:147 already returns API error: <description>. Adapt: type the error and stop discarding Yahoo's error Code.
4 #2900 30m/15m interval in messages Error text uses user-requested interval, notes resampled from 15m N/A. go-yfinance never substitutes 30m→15m (it fetches 30m directly); there is no internal interval to leak. Recorded below.
5 #2896 Lookup error handling self.tickerself.query in _fetch_lookup error paths (was AttributeError) Adapt. Go has no such crash, but its lookup API error omits the query entirely. Include l.query in the error message for message parity.
6 #2906 null result in _fetch_info result.get(quote, {}).get("result") or [] — JSON null treated as empty N/A by construction, lock with test. Go's encoding/json decodes null into a nil slice, and len(nil) == 0 takes the not-found path. Add a regression test.
7 #2907/#2943/#2908 GBp/ZAc/ILA leak _standardise_currency returns prices_scaled; after repair, prices and dividends are scaled back and metadata currency restored Port — main gap. go-yfinance has no currency standardisation at all (no GBp/ZAc/ILA handling anywhere). New pkg/repair/currency.go + revert step in Repairer.Repair.
8 #2908/#2943 volume cross-check for unit-switch/split denoise_volume helper; boundary volume-change gate; new thresholds 1+(split_max-1+pct)*0.6 and *0.333 Port (adapted). Go's fixPricesSuddenChange (pkg/repair/unit_mixup.go:200) uses the old formula (max+1+pct)*0.5 and has no volume logic. Port formula + zero-volume guard + boundary volume gate. Split repair (pkg/repair/split.go) gets the matching volume-sign requirement.
9 #2908 skip unit/split repair for FX if '=' not in self.ticker gate in history() Port. Gate repairUnitMixups/repairStockSplits on strings.Contains(r.opts.Ticker, "=") in pkg/repair/repair.go.
10 #2908 invalid OHLC rows (Close < Low etc.) repaired via _fix_zeroes Offending column pairs marked bad and reconstructed Adapt. Go's normalizeOHLCBounds (pkg/repair/zeroes.go:219) silently widens High/Low without marking Repaired. Detect inconsistent pairs, repair from good values, set Repaired=true.
11 #2908 Low/High recalculated after per-column 100x repair After multiplying a column by m: m>1 → Low=min(Open,Close); m<1 → High=max(Open,Close) Port into detectAndCorrectMixups (pkg/repair/unit_mixup.go:154). This is exactly what the new ASAI.L fixture asserts.
12 #2908/#2943 dividend-repair thresholds & branches div_too_big fals 0.25→0.5; div_too_small fals 0.15→0.11; .TA true-threshold 0.74; adj_missing never cluster-error; 2 new repair branches; intraday resample-to-1d + back-port Port partially. Go's pkg/repair/dividend.go is a simplified analogue. Port the threshold values where the corresponding check exists; assess the two new branches and the intraday resample in Phase 0.
13 #2908 reconstruction changes (DBSCAN ratio pruning, Adj-Close post-anchor, newest-first groups, Repaired=any agg) All inside _reconstruct_intervals_batch N/A. Go repair has no sub-interval reconstruction; repairs are in-place statistical fills. The scikit-learn dependency exists only for this DBSCAN step, so it is N/A too.
14 #2908 _fix_unit_switch writes standardised currency into metadata GBp→GBP etc. written to history_metadata['currency'] when a switch was repaired N/A as metadata (Go does not mutate user-visible chart metadata), but the currency mapping table is consumed by item 7.
15 #2897 read-only Adj Close numpy array .copy() when not writeable N/A. Python-runtime (pandas copy-on-write) only.
16 #2891/#2915 timedelta/numpy deprecations, multi-day _dts_in_same_interval Explicit m/h/d branches in _interval_to_timedelta N/A. Go has no interval→duration utility and no resample path that compares interval spans; the deprecation parts are pandas-only.
17 format_history_metadata always converts, guards missing exchangeTimezoneName tradingPeriodsOnly param removed N/A. Go parses chart metadata into typed structs (models.ChartMeta); there is no deferred-formatting flag to remove.
18 #2936 download() tz-contract docstring Docs only Docs. Mirror the contract note in pkg/multi docs if the behavior matches; otherwise record the difference.
19 Packaging/CI: pyproject.toml, uv, ruff pin, CHANGELOG Python packaging only N/A. Nothing to port.
20 Version bump 1.5.2 → 1.6.0 version.py, meta.yaml Port. Parity strings in README.md, docs/index.md, mkdocs.yml; new release notes + this progress doc.

Known pre-existing gaps recorded (out of v1.6.0 scope)

  • CommonScreenerFields does not exist in Go even though three doc comments in pkg/models/screener_const.go reference it; python keeps region/exchange etc. there. Not introduced by v1.6.0 — track separately.
  • pkg/multi has no repair wiring (Repair param unused there). Python's download() supports repair; not changed by v1.6.0 — track separately.
  • go-yfinance issues one chart request per history() call; python's test_no_expensive_calls_introduced (re-enabled upstream) pins python to exactly two (tz probe + data). Behavioral difference, unchanged by v1.6.0.

Implementation Plan

Phase 0: Verify assessments

  • [x] Read pkg/repair/dividend.go in full and map upstream item 12 onto it. Finding: Go analyzes each dividend event independently (analyzeDividendWithOptionsdividendStatus); python's cluster-level classification (group dividends, classify the cluster with true/false-positive ratio thresholds) has no Go analogue. Therefore the cluster threshold changes (div_too_big fals 0.25→0.5, div_too_small fals 0.15→0.11, .TA true-threshold 0.74, adj_exceeds_prices n/n_fail redefinition, adj_missing cluster continue) are N/A — structural. Two per-event changes DO map: (a) the pre/post false-positive test — Go's isDividendTooSmall uses dayMove = prevClose − Close; upstream final uses the ex-div bar's own Open − Close ("price recovered by end of session"); (b) the new "too-small div and missing div-adjust" combined branch — Go's repairDividends if/else picks IsMissingAdj first and applies the adjustment with the still-wrong (100x-too-small) dividend. The "pre-split & too-small div-adjust" branch needs split-cluster awareness Go lacks — N/A. Intraday resample-to-1d + back-port: N/A — Go's per-event logic is already interval-aware (isIntradayInterval) and has no daily-cluster machinery.
  • [x] Read pkg/repair/split.go. Finding: the boundary-volume gate goes in repairSplitAtIndex, guarding the applySplitCorrection call: require an abnormal split-date volume change of matching sign (price drop ⇒ volume jump, price jump ⇒ volume drop). Go's detection threshold ((|expectedChange| + largestNormalChange)/2, a distance tolerance in pct-change space) is parameterized differently from python's ratio-space detection threshold, so the 0.5→0.6 weight change does not transfer to split.go — only to fixPricesSuddenChange, whose formula is directly analogous.
  • [x] Confirm filterValidBars (pkg/ticker/history.go:292) gives the repairer NaN-free bars unless KeepNA=true. Confirmed; per-bar validPrice guards cover the KeepNA path, so python's NaN-row split/rejoin is redundant in Go. denoiseVolume will skip non-positive/NaN inputs.
  • [x] Confirm items 4, 6, 15, 16, 17 N/A rationales. Confirmed against pkg/ticker/history.go (no interval substitution), Go encoding/json null→nil-slice semantics, and the absence of any interval→duration or deferred-metadata-formatting code.
  • [x] New finding (extends item 11): detectAndCorrectMixups (pkg/repair/unit_mixup.go:154) returns one correction per row and applyUnitCorrection multiplies the whole bar, so a bar like ASAI.L's (Open/High correct, Low/Close/AdjClose 100x too small) would corrupt the good columns. Python repairs per cell. Phase 5 must switch to per-cell corrections; the ASAI golden fixture locks this.

Phase 1: Data tables (screener fields, balance-sheet keys)

  • [x] pkg/models/screener_const.go: add "dividendyield" and "dividendpershare.lasttwelvemonths" to EquityScreenerFields["profitability"].
  • [x] internal/endpoints/endpoints.go: extend BalanceSheetKeys with "FixedMaturityInvestments", "EquityInvestments", "NetLoan", "DeferredAssets". Python inserts after "CashCashEquivalentsAndFederalFundsSold"; if that anchor is absent in the Go list, append the four keys as a block and note the ordering difference here (ordering only affects statement column order).
  • [x] Tests: extend the existing screener-query validation test to accept an EquityQuery on "dividendyield", and the financials key test (or add one) asserting the four keys are requested. Run go test ./pkg/models/... ./pkg/ticker/... ./internal/....
  • [x] Commit: feat: add v1.6.0 screener fields and balance sheet keys

Phase 2: Error-handling parity

  • [x] pkg/lookup/lookup.go (fetch, error branch 4): change fmt.Errorf("lookup API error: %s - %s", code, desc) to include the query, mirroring python's final format: fmt.Errorf("%s: 'lookup' fetch returned error: %s - %s", l.query, code, desc). Update/extend lookup_test.go to assert the query appears in the message (upstream TestLookupErrorHandling equivalent).
  • [x] pkg/ticker/history.go:147: stop discarding the chart error code. Add a typed error in pkg/client/errors.go (e.g. ChartAPIError{Symbol, Code, Description string}) whose Error() is "$<symbol>: <description>" — matching upstream's final message shape where Yahoo's reason is the entire rationale, no "possibly delisted" prefix. Keep errors.As support.
  • [x] pkg/ticker/info_test.go: add a regression test feeding {"quoteSummary":{"result":null,"error":{...}}} into parseInfoResponse, asserting a clean error (not a panic) — locks upstream #2906's contract.
  • [x] Same null-result regression for Quote() if a seam allows; otherwise cover via the shared response model test.
  • [x] Run go test ./pkg/lookup/... ./pkg/ticker/... ./pkg/client/....
  • [x] Commit: feat: surface yahoo chart error reason and lookup query in errors

Phase 3: Repair — currency standardisation (GBp/ZAc/ILA)

Upstream final shape (after #2907 + regression fix f854309): repair math runs in the main currency, but user-visible prices and dividends are scaled back unconditionally, and the reported currency stays GBp/ZAc/ILA.

  • [x] New pkg/repair/currency.go:
  • var currencyConversions = map[string]currencyConversion{"GBp": {0.01, "GBP"}, "ZAc": {0.01, "ZAR"}, "ILA": {0.01, "ILS"}}
  • func standardiseCurrency(bars []models.Bar, currency string) ([]models.Bar, string, bool) returning (bars, standardCurrency, pricesScaled); multiplies Open/High/Low/Close/AdjClose and Dividends by the factor when the currency is a sub-unit, else returns input unchanged with pricesScaled=false.
  • func revertCurrency(bars []models.Bar, originalCurrency string) []models.Bar dividing prices and dividends by the factor (unconditional dividend revert — this is exactly the f854309 regression fix).
  • [x] pkg/repair/repair.go (*Repairer).Repair: call standardiseCurrency first (using r.opts.Currency), run the pipeline with the standardised currency, call revertCurrency before returning when pricesScaled. User-visible output must be in the original sub-currency.
  • [x] Consolidate the currencyDivide (100 / KWF 1000) logic duplicated at pkg/repair/unit_mixup.go:44 and pkg/repair/dividend.go:51,393,438 into one helper in currency.go.
  • [x] Tests (pkg/repair/currency_test.go): GBp round-trip — synthetic bars in pence with a 100x error; after Repair, prices are repaired and still in pence, dividends still in pence (regression for upstream #2907+f854309); ZAc and ILA table cases; non-sub-currency passthrough.
  • [x] Run go test ./pkg/repair/....
  • [x] Commit: feat: keep GBp/ZAc/ILA prices in original unit through repair

Phase 4: Repair — sudden-change detection with volume cross-check

All in pkg/repair/unit_mixup.go unless noted. Current Go code uses the old threshold formula and no volume signal.

  • [x] Zero-volume guard: at the top of fixPricesSuddenChange, if every bar.Volume == 0, return bars unchanged (upstream: "No Volume data, cannot repair").
  • [x] Threshold formula: replace threshold := (changeMax + 1.0 + largestChangePct) * 0.5 with threshold := 1 + (changeMax - 1 + largestChangePct) * 0.6 (upstream 8b85f90).
  • [x] New helper denoiseVolume(vol []float64) []float64 (port of upstream's nested denoise_volume): window W = min(9, len) forced odd; zeros back-filled then forward-filled; sliding-window median (ignoring NaN pads). Place in unit_mixup.go or a new volume.go.
  • [x] Boundary volume gate in fixPricesSuddenChange: at the detected switch index i, compute boundaryVolChange = denoised(vol[i:])[0] / denoised(vol[:i])[last>0], volume threshold volThreshold := 1 + (changeMax - 1 + largestVolChgPct) * 0.333 where largestVolChgPct comes from IQR-filtered 1-bar volume changes (mean/std, 5*sdPct, interday multipliers ×3 for interday non-1d, ×2 more for 1mo/3mo). Unit-switch semantics (this function's only current caller): skip the repair if the boundary volume change is itself abnormal in the direction that suggests a real corporate event (boundaryVolChange < 1/volThreshold with price up, or > volThreshold with price down).
  • [x] Split-side gate in pkg/repair/split.go: a candidate split boundary is only accepted if the boundary volume change is abnormal with matching sign (price drop ⇒ volume jump, price jump ⇒ volume drop), using the same denoiseVolume + volThreshold. Exact insertion point comes from Phase 0.
  • [x] FX skip (item 9): in pkg/repair/repair.go Repair, skip repairUnitMixups and repairStockSplits when strings.Contains(r.opts.Ticker, "=") (volume-dependent repairs are meaningless for FX). _fix_zeroes/capital-gains still run.
  • [x] Tests (pkg/repair/unit_mixup_test.go, split_test.go): genuine unit switch with flat volume → repaired; identical price jump with a matching volume jump (real split pattern) → unit-switch repair skipped; split with flat volume → split repair skipped; all-zero volume → untouched; FX ticker (EURUSD=X) → unit/split repair skipped.
  • [x] Run go test ./pkg/repair/....
  • [x] Commit: feat: volume cross-check for unit-switch and split repair

Phase 5: Repair — invalid OHLC rows and per-column Low/High recalc

  • [x] pkg/repair/zeroes.go: extend invalid detection with upstream #2908's consistency rules — treat as bad: (Close < Low → Close+Low), (Close > High → Close+High), (Open < Low → Open+Low), (Open > High → Open+High). Route these rows through the existing goodOHLCValues/fillInvalidOHLC repair and set Repaired=true (today normalizeOHLCBounds silently widens bounds without marking).
  • [x] pkg/repair/unit_mixup.go: switch detectAndCorrectMixups from per-row to per-cell corrections ([][]float64, one factor per OHLC/AdjClose cell) and apply them per field, matching python's _fix_unit_random_mixups (Phase 0 finding: the current whole-row multiply corrupts good columns). After a per-cell repair with factor m on any price column, recalculate the row: if m > 1 → Low = min(Open, Close, Low); if m < 1 → High = max(Open, Close, High) (upstream rationale: Yahoo derived Low/High from the corrupt column).
  • [x] Golden fixture: create pkg/repair/testdata/ (first fixture in the repo) with ASAI-L-1h-bad-unit.csv and ASAI-L-1h-bad-unit-fixed.csv copied from upstream tests/data/ (20 hourly rows; the two files differ in exactly one row where Low/Close/AdjClose are 100x too small). Add TestRepairUnitSwitchASAIGolden in unit_mixup_test.go: load the bad CSV, run the unit-switch repair path with Interval: "1h", currency GBp, compare O/H/L/C to the fixed CSV within rtol 1e-7. This exercises the per-column 100x repair plus the Low recalc (Low = 58.5, not 60.0).
  • [x] Run go test ./pkg/repair/....
  • [x] Commit: feat: repair inconsistent OHLC rows and recalc bounds after column repair

Phase 6: Repair — dividend-adjust updates

Scope fixed by the Phase 0 mapping: the cluster-level threshold changes (div_too_big 0.25→0.5, div_too_small 0.15→0.11, .TA 0.74, adj_exceeds_prices redefinition, adj_missing cluster continue) and the "pre-split & too-small div-adjust" branch and the intraday resample+back-port are N/A — structural (Go analyzes per event, python per cluster). Two per-event changes port:

  • [x] isDividendTooSmall (pkg/repair/dividend.go:184): replace the dayMove argument (prevClose − Close) with the ex-div bar's own Open − Close in the pre/post false-positive test, per upstream (df2['Open'].iloc[div_idx] - df2['Close'].iloc[div_idx]) < 0.2*drop_wo_vol.
  • [x] repairDividends switch: add a combined branch before the IsMissingAdj branch — when IsMissingAdj && IsTooSmall, first multiply the dividend by currencyDivide, then apply the missing adjustment with the corrected dividend (upstream "too-small div and missing div-adjust": adj_correction = 1 − pct*currency_divide). Today the IsMissingAdj-first ordering bakes the 100x-too-small dividend into the adjustment.
  • [x] Tests: pre/post false-positive test uses Open−Close (a bar whose price recovers intraday must not be classified too-small under PrePost && intraday); combined too-small+missing-adj case yields the corrected dividend AND the adjustment from the corrected value.
  • [x] Run go test ./pkg/repair/....
  • [x] Commit: feat: update dividend repair thresholds to v1.6.0

Phase 7: Docs and version bump

  • [x] README.md:14: parity line → **Python yfinance v1.6.0 Parity** with a one-line feature summary (volume-cross-checked price repair, GBp/ZAc/ILA preservation, new screener fields and balance-sheet keys, Yahoo-reason error messages). Then cp README.md docs/index.md.
  • [x] docs/releases/RELEASE_NOTES_v1.6.0.md: follow the existing format; list ported items, adapted items, and the N/A table (reconstruction/DBSCAN, 30m substitution, packaging) with one-line reasons.
  • [x] mkdocs.yml: insert - v1.6.0: releases/RELEASE_NOTES_v1.6.0.md at the top of the Releases: block and - v1.6.0 Progress: development/v1.6.0-progress.md at the top of the progress list.
  • [x] docs/development/release-readiness-checklist.md §1: add the v1.6.0 version-decision note.
  • [x] make docs (regenerates docs/api/*.md — required because Phases 2–3 add exported symbols), make docs-build to verify mkdocs.
  • [x] pkg/multi doc comment: state the returned-timestamp timezone contract (upstream #2936 equivalent) if it matches Go behavior; otherwise record the difference in this doc.
  • [x] Commit: docs: advance parity baseline to python yfinance v1.6.0

Implementation outcome notes (deviations from the plan as written)

  • Balance-sheet keys: the python anchor CashCashEquivalentsAndFederalFundsSold does not exist in Go's BalanceSheetKeys (Go has CashCashEquivalentsAndShortTermInvestments), so the four new keys were appended as a contiguous block at the end. Only statement column order is affected.
  • Split repair: porting the volume gate exposed a pre-existing Go bug — expectedSplitChange returned inverted signs (+0.5 for a 2:1 split whose unadjusted price change is −0.5), so repairStockSplits never detected genuinely unadjusted splits. Fixed alongside the gate and covered by new end-to-end tests. Go's split detection threshold formula (a distance tolerance in pct-change space) was left unchanged as planned.
  • Per-cell 100x repair: implemented as a two-pass scheme — the coarse round-to-20≈100 test flags rows, then within a flagged row every cell whose ratio to its local median is decisively on the 100x side (>10 or <0.1, the geometric midpoint of 1 and 100) is corrected. The pure per-cell coarse test was too brittle against Go's median-filter edge handling; python reaches the same per-cell outcome via mode="nearest" edge padding.
  • Currency standardisation scales prices and dividends symmetrically on entry and revert. Python tracks a separate div_scaled flag on entry but reverts dividends unconditionally (the f854309 regression fix); the symmetric Go version has the same user-visible behavior with less state.
  • download() tz contract (upstream #2936): N/A as a doc change — Go returns time.Time instants (absolute, timezone-carrying), so python's naive-vs-converted index contract has no Go equivalent to document.
  • Dividend combined branch: implemented by scaling the dividend before the existing fixMissingDivAdj, which then derives the adjustment from the corrected value — equivalent to upstream's adj_correction = 1 − pct*currency_divide.

Phase 8: Verification and close-out

  • [x] go build ./..., go vet ./..., GOCACHE=/tmp/go-build-cache go test ./... — all green (17 packages ok).
  • [x] GOLANGCI_LINT_CACHE=/tmp/golangci-lint-cache golangci-lint run ./... — 0 issues.
  • [x] Live smoke against XDEV.L (GBp): 3-month history fetched with and without Repair; both return 66 bars with identical last close (6255.00 pence) — repair preserves the sub-unit scale (upstream test_repair_gbp_not_converted equivalent). Run 2026-08-17.
  • [x] Mark every checklist item above done, skipped (with reason), or N/A.
  • [x] Update the "Last Updated" date at the top of this document.
  • [ ] Follow docs/development/release-readiness-checklist.md before merge/tag (left open: done at merge/tag time).


Post-merge cross-verification round (parallel-verify)

After the initial implementation (Phases 0-8 above), the branch was independently re-verified against the upstream beac22d..0af231f diff by two blind parallel agents (Claude Code, Codex), each deriving the oracle independently from upstream and reading only this repo — not each other's findings. Results were then reconciled by reading the upstream code directly for every disputed or single-sourced item. 8 items were accepted; all are fixed in this round except one, which is recorded as a documented gap.

Fixed

  1. Unit-switch repair no longer scales Volume (unit_mixup.go applyUnitSwitchCorrection) — a currency-unit switch does not change the share count. Upstream's shared _fix_prices_sudden_change call for unit switches passes correct_dividend=True but never correct_volume=True; only the split-repair call passes both. Go's unit-switch path was scaling Volume, which corrupted returned volume by the same factor as the price fix. (high — found independently by one verifier)
  2. Unit-switch and split repairs now scale Dividends by the same factor as prices (applyUnitSwitchCorrection, applySplitCorrection) — found while confirming finding 1 against upstream: both call sites pass correct_dividend=True, and the repaired range's dividends must move with the price correction. Neither Go function touched Dividends before this round. (found during judgment, not flagged by either verifier)
  3. Split detection threshold now matches upstream's shared formula (split.go repairSplitAtIndex) — replaced the old symmetric distance-band check (|expectedChange|+largestNormalChange)/2 with upstream's 1 + (splitMax-1+largestNormalChange)*0.6, compared as a ratio-space threshold (beyond threshold in the direction implied by the split), not a tolerance band around one assumed value. The old formula accepted unadjusted-split candidates with materially weaker price drops than upstream would. (med — both verifiers independently flagged this)
  4. Currency standardisation now scales dividends conditionally on entry (currency.go standardiseCurrency / new averageDividendToPrevCloseRatio) — upstream only multiplies Dividends by the sub-unit factor on entry when the average dividend/prevClose ratio (using the now-scaled Close) exceeds 1, i.e. when the raw dividend is implausible unless it's still in the sub-unit. The unconditional entry-scaling previously in Go breaks the common LSE pattern where the dividend is already reported in the main currency even though prices are in pence. Revert stays unconditional (unchanged — matches upstream's f854309 regression fix). (med)
  5. Price-noise tolerance now widens for coarser intervals (unit_mixup.go fixPricesSuddenChange) — added the missing x3 (1wk) / x6 (1mo, 3mo) multiplier on largestChangePct, matching upstream's interday and interval != '1d' gate. Previously only the volume-noise threshold had this multiplier; the price-detection threshold did not. (low)
  6. 5d no longer gets the interday noise multiplier (volume.go → shared intervalNoiseMultiplier) — upstream's interday set is exactly {1d, 1wk, 1mo, 3mo}; 5d was previously included in the x3 bucket by mistake, loosening the volume threshold for 5-day bars. (low)
  7. Standard deviation now uses population variance (ddof=0) in fixPricesSuddenChange, volumeChangeThreshold, and repairSplitAtIndex — matches upstream np.std's default (stats.Std(..., 1)..., 0) at all three call sites; the Go code previously used sample variance, which overestimates volatility for small windows and loosens every threshold that depends on it. (low)
  8. splitVolumeConfirms no longer vetoes on unusable boundary volume (volume.go) — when the boundary volume change can't be computed (no positive volume on one side), upstream proceeds without the cross-check rather than blocking the repair; only all-zero volume across the whole series disables split repair entirely (kept unchanged, matches upstream's (vol==0.0).all(): return df guard for the whole function). (low)

Documented gap (not fixed)

  • prices_in_subunits freshness heuristic (currency.go standardiseCurrency) — upstream skips price scaling when a recent (< 30 days old) bar's Close, compared against the ticker's live regularMarketPrice from chart metadata, shows Yahoo already returned main-unit prices (a rare Yahoo-side bug). go-yfinance's repair layer receives only []models.Bar and repair Options — it has no access to live chart metadata at this layer — so price standardisation in Go always assumes sub-unit prices need scaling. This only diverges from upstream in the rare edge case the heuristic exists for; the common case (prices genuinely in sub-units) is unaffected because the round-trip (standardise → repair → revert) is symmetric either way. Fixing this would require threading chart metadata (or at least regularMarketPrice) into repair.Options, which is a larger interface change than this verification round's scope. (low, tracked here for a future pass)

Verification after fixes

  • go build ./..., go vet ./... — clean.
  • GOCACHE=/tmp/go-build-cache go test ./... — all 17 packages pass, including new regression tests for each fixed item (pkg/repair/verify_fixes_test.go, plus updates to currency_test.go for the conditional-dividend-scaling contract).
  • GOLANGCI_LINT_CACHE=/tmp/golangci-lint-cache golangci-lint run ./... — 0 issues.

Status Legend

  • [x] Done
  • [ ] Pending
  • N/A — verified not applicable to Go, with the reason recorded inline