Summary
backtest.DataReport.Results is a plain map[string][]*DataStrategyResult, mutated by AssetBegin and Write with no synchronization. Backtest.worker runs one goroutine per worker (Backtest.Workers), each calling report.AssetBegin / report.Write / report.AssetEnd concurrently for different assets. With Workers > 1 and DataReport as the report, this is a concurrent map write.
This is the same bug class fixed for HTMLReport in #393 / commit b396ad5 (which added a sync.Mutex to backtest/html_report.go), but DataReport was never touched.
Reproduction
go test -race with a Backtest configured with Workers: 8, backtesting 20+ assets, using NewDataReport() as the report, reports a concurrent map write in DataReport.Write / DataReport.AssetBegin immediately. In a non-race build this is fatal error: concurrent map writes — a process crash, not silent corruption.
Impact
DataReport is the report type mcp/strategy.go builds its backtest results on, so this is reachable from the MCP server's runBacktest tool, not just direct library use.
Root cause: the Report interface (backtest/report.go) has no documented concurrency contract, so nothing signals to an implementer that AssetBegin/Write/AssetEnd can be called concurrently across assets. DataReport made the same mistake HTMLReport did, and any future Report implementation is likely to repeat it.
Proposed fix
Rather than adding a mutex to DataReport alone (which just repeats the HTMLReport fix a second time and leaves the interface trap for the next implementation), centralize serialization in Backtest.worker itself so no Report implementation needs to manage its own locking, and document the contract on the Report interface.
CI's test task also doesn't run go test -race, which is why this wasn't caught automatically.
Summary
backtest.DataReport.Resultsis a plainmap[string][]*DataStrategyResult, mutated byAssetBeginandWritewith no synchronization.Backtest.workerruns one goroutine per worker (Backtest.Workers), each callingreport.AssetBegin/report.Write/report.AssetEndconcurrently for different assets. WithWorkers > 1andDataReportas the report, this is a concurrent map write.This is the same bug class fixed for
HTMLReportin #393 / commit b396ad5 (which added async.Mutextobacktest/html_report.go), butDataReportwas never touched.Reproduction
go test -racewith aBacktestconfigured withWorkers: 8, backtesting 20+ assets, usingNewDataReport()as the report, reports a concurrent map write inDataReport.Write/DataReport.AssetBeginimmediately. In a non-race build this isfatal error: concurrent map writes— a process crash, not silent corruption.Impact
DataReportis the report typemcp/strategy.gobuilds its backtest results on, so this is reachable from the MCP server'srunBacktesttool, not just direct library use.Root cause: the
Reportinterface (backtest/report.go) has no documented concurrency contract, so nothing signals to an implementer thatAssetBegin/Write/AssetEndcan be called concurrently across assets.DataReportmade the same mistakeHTMLReportdid, and any futureReportimplementation is likely to repeat it.Proposed fix
Rather than adding a mutex to
DataReportalone (which just repeats theHTMLReportfix a second time and leaves the interface trap for the next implementation), centralize serialization inBacktest.workeritself so noReportimplementation needs to manage its own locking, and document the contract on theReportinterface.CI's
testtask also doesn't rungo test -race, which is why this wasn't caught automatically.