Summary
Add comprehensive tests that lock down the current plotting output before the backend abstraction refactor begins. These tests serve as regression gates — every subsequent PR in the refactor (#2–#8) must pass them unchanged.
Must be merged before: #2
Motivation
The current plotting test coverage is effectively zero. The only existing test (test_portfolio.py:test_plots) is a smoke test that discards the return value. The backend refactor will rewrite how traces are created internally (protocol methods vs direct Plotly calls) but the output must remain identical. We need tests that capture the output shape so regressions are caught immediately.
Test file
tests/test_plotting.py — dedicated file with its own minimal fixtures.
Fixtures needed
- Simple Series/DataFrame (5–10 rows, datetime-indexed) for generic/accessor tests
- DataFrame with OHLC + volume columns for OHLCV tests
- A minimal
Portfolio with deterministic orders so we get predictable Orders, Trades, and Drawdowns records to assert against
Tier 1: Figure factory (utils/figure.py)
Tests for make_figure() and make_subplots():
make_figure() returns Figure when use_widgets=False
make_figure() returns FigureWidget when use_widgets=True
- Default layout from settings is applied
make_subplots(rows=N, cols=M) returns correct type and subplot grid structure
Small scope, high value — if the protocol mixin (PR #2) breaks these, everything breaks.
Tier 2: Core plot methods (migration targets)
These are the exact methods rewritten in #5 and #6. Each test is a direct regression gate.
Per-method assertions
For every method below, assert:
- Return type is
BaseFigure (i.e. go.Figure or go.FigureWidget)
len(fig.data) — expected trace count
- Trace types —
type(fig.data[i]) is go.Scatter, go.Candlestick, etc.
- Trace names —
fig.data[i].name matches expected
- Trace data — x/y values match expected (
np.testing.assert_array_equal or pytest.approx for computed values)
len(fig.layout.shapes) — for methods that add shapes (zones, hlines)
Methods to cover
| Method |
Key behavioral variants to test |
OHLCVAccessor.plot() |
plot_type='ohlc' vs 'candlestick'; show_volume=True adds Bar trace + 2-row subplot |
Orders.plot() |
Close line trace present when close data exists; buy/sell marker traces with correct x/y from order records |
Trades.plot() |
Entry/exit marker traces; profit/loss zone shapes when plot_zones=True; shape count |
Trades.plot_pnl() |
Profit/loss/open scatter traces; hline shape at y=0; pct_scale affects y data |
Drawdowns.plot() |
Peak/valley/recovery marker traces; decline/recovery zone shapes; top_n filtering reduces trace data |
Ranges.plot() |
Start/end marker traces; open/closed zone shapes |
GenericAccessor.plot() |
Trace count matches number of columns; data matches input; return_fig=True returns figure, False returns Scatter wrapper |
GenericAccessor.plot_against() |
Correct number of fill traces for positive/negative areas |
What NOT to assert
- Colors, marker sizes, opacity — styling details
- Layout dimensions (width/height)
- Trace ordering within
fig.data
- Legend formatting
- Plotly kwargs passthrough
Tier 3: Trace wrapper classes (generic/plotting.py)
These are internal building blocks that may be modified during the refactor. Tests here are valuable but may need updating if we intentionally change the wrapper layer.
Focus on the most commonly used wrappers:
| Wrapper |
Assertions |
Scatter |
Creates go.Scatter traces; count matches columns; data matches input; update() changes trace data |
Bar |
Creates go.Bar traces; count matches columns; data matches input; update() works |
Histogram |
Creates go.Histogram traces; update() works |
Heatmap |
Creates go.Heatmap trace; data matches input |
Skip Gauge, Box, Volume — Plotly-only, won't be migrated, lower risk.
Tier 4: PlotsBuilderMixin.plots() via Portfolio
The orchestrator that will be modified to use create_figure(). Test via Portfolio.plots():
- Returns
BaseFigure
- Correct total trace count when requesting specific subplots (e.g.
subplots=['orders', 'trades'])
- Subplot filtering by name works (requesting
['orders'] only produces order-related traces)
- Subplot filtering by tags works
column= selection works for multi-column portfolios
- Raises for multi-column data without
column param
- Grouped vs ungrouped variants (using
group_by)
Tier 5: Portfolio plot_* methods (smoke tests)
Thin wrappers that delegate to Tier 2 methods. Only verify each runs without error and returns a BaseFigure. No deep assertions.
Methods: plot_orders, plot_trades, plot_trade_pnl, plot_asset_flow, plot_cash_flow, plot_assets, plot_cash, plot_asset_value, plot_value, plot_cum_returns, plot_drawdowns, plot_underwater, plot_gross_exposure, plot_net_exposure
Test with pf (ungrouped), pf_grouped, and pf_shared variants where applicable.
Acceptance criteria
Summary
Add comprehensive tests that lock down the current plotting output before the backend abstraction refactor begins. These tests serve as regression gates — every subsequent PR in the refactor (#2–#8) must pass them unchanged.
Must be merged before: #2
Motivation
The current plotting test coverage is effectively zero. The only existing test (
test_portfolio.py:test_plots) is a smoke test that discards the return value. The backend refactor will rewrite how traces are created internally (protocol methods vs direct Plotly calls) but the output must remain identical. We need tests that capture the output shape so regressions are caught immediately.Test file
tests/test_plotting.py— dedicated file with its own minimal fixtures.Fixtures needed
Portfoliowith deterministic orders so we get predictable Orders, Trades, and Drawdowns records to assert againstTier 1: Figure factory (
utils/figure.py)Tests for
make_figure()andmake_subplots():make_figure()returnsFigurewhenuse_widgets=Falsemake_figure()returnsFigureWidgetwhenuse_widgets=Truemake_subplots(rows=N, cols=M)returns correct type and subplot grid structureSmall scope, high value — if the protocol mixin (PR #2) breaks these, everything breaks.
Tier 2: Core plot methods (migration targets)
These are the exact methods rewritten in #5 and #6. Each test is a direct regression gate.
Per-method assertions
For every method below, assert:
BaseFigure(i.e.go.Figureorgo.FigureWidget)len(fig.data)— expected trace counttype(fig.data[i])isgo.Scatter,go.Candlestick, etc.fig.data[i].namematches expectednp.testing.assert_array_equalorpytest.approxfor computed values)len(fig.layout.shapes)— for methods that add shapes (zones, hlines)Methods to cover
OHLCVAccessor.plot()plot_type='ohlc'vs'candlestick';show_volume=Trueadds Bar trace + 2-row subplotOrders.plot()Trades.plot()plot_zones=True; shape countTrades.plot_pnl()pct_scaleaffects y dataDrawdowns.plot()top_nfiltering reduces trace dataRanges.plot()GenericAccessor.plot()return_fig=Truereturns figure,Falsereturns Scatter wrapperGenericAccessor.plot_against()What NOT to assert
fig.dataTier 3: Trace wrapper classes (
generic/plotting.py)These are internal building blocks that may be modified during the refactor. Tests here are valuable but may need updating if we intentionally change the wrapper layer.
Focus on the most commonly used wrappers:
Scattergo.Scattertraces; count matches columns; data matches input;update()changes trace dataBargo.Bartraces; count matches columns; data matches input;update()worksHistogramgo.Histogramtraces;update()worksHeatmapgo.Heatmaptrace; data matches inputSkip
Gauge,Box,Volume— Plotly-only, won't be migrated, lower risk.Tier 4:
PlotsBuilderMixin.plots()via PortfolioThe orchestrator that will be modified to use
create_figure(). Test viaPortfolio.plots():BaseFiguresubplots=['orders', 'trades'])['orders']only produces order-related traces)column=selection works for multi-column portfolioscolumnparamgroup_by)Tier 5: Portfolio
plot_*methods (smoke tests)Thin wrappers that delegate to Tier 2 methods. Only verify each runs without error and returns a
BaseFigure. No deep assertions.Methods:
plot_orders,plot_trades,plot_trade_pnl,plot_asset_flow,plot_cash_flow,plot_assets,plot_cash,plot_asset_value,plot_value,plot_cum_returns,plot_drawdowns,plot_underwater,plot_gross_exposure,plot_net_exposureTest with
pf(ungrouped),pf_grouped, andpf_sharedvariants where applicable.Acceptance criteria
tests/test_plotting.pyexists with organized test classes