13 KiB
Implementation Plan: Dashboard Enhancements
Overview
Incremental implementation of four dashboard enhancements: sentiment drill-down, fundamentals drill-down, chart zoom/crosshair, and S/R zone clustering. Each feature area is built backend-first (model → service → schema → router) then frontend, with tests alongside implementation. All changes are additive to existing components.
Tasks
-
1. Sentiment drill-down — backend
-
1.1 Add
reasoningandcitations_jsoncolumns toSentimentScoremodel and create Alembic migration- Add
reasoning: Mapped[str] = mapped_column(Text, nullable=False, default="")andcitations_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")toapp/models/sentiment.py - Create Alembic migration with
server_defaultso existing rows are backfilled - Requirements: 1.1, 1.2, 1.3
- Add
-
1.2 Update
OpenAISentimentProviderto extract reasoning and citations from OpenAI response- Add
reasoningandcitationsfields to theSentimentDatadataclass - Extract
reasoningfrom the parsed JSON response body - Iterate
response.outputitems forurl_citationannotations, collect{"url": ..., "title": ...}dicts - Return empty citations list when no annotations exist (no error)
- Requirements: 1.1, 1.2, 1.4
- Add
-
1.3 Update
sentiment_service.store_sentiment()to persist reasoning and citations- Accept
reasoningandcitationsparameters - Serialize citations to JSON string before storing
- Requirements: 1.3
- Accept
-
1.4 Update sentiment schema and router to include reasoning and citations in API response
- Add
CitationItemmodel andreasoning/citationsfields toSentimentScoreResultinapp/schemas/sentiment.py - Deserialize
citations_jsonwhen building the response, catchJSONDecodeErrorand default to[] - Requirements: 1.3
- Add
-
* 1.5 Write property tests for sentiment reasoning and citations extraction
- Property 1: Sentiment reasoning extraction — Generate random JSON with reasoning fields, verify extraction
- Validates: Requirements 1.1
- Property 2: Sentiment citations extraction — Generate mock OpenAI responses with 0–10 annotations, verify citations list
- Validates: Requirements 1.2, 1.4
-
* 1.6 Write property test for sentiment data round-trip
- Property 3: Sentiment data round-trip — Generate random reasoning + citations, store, retrieve via test client, compare
- Validates: Requirements 1.3
-
-
2. Sentiment drill-down — frontend
-
2.1 Add
CitationItem,reasoning, andcitationsfields toSentimentScoretype infrontend/src/lib/types.ts- Requirements: 1.3, 2.2, 2.3
-
2.2 Add expandable detail section to
SentimentPanel- Add
useState<boolean>(false)for expand/collapse toggle - Render chevron toggle button below summary metrics
- When expanded: show reasoning text (or "No reasoning available" placeholder if empty) and citations as clickable
<a>links - When collapsed: hide reasoning and citations, keep summary metrics visible
- Default to collapsed state on initial render
- Preserve glassmorphism UI style
- Requirements: 2.1, 2.2, 2.3, 2.4, 2.5
- Add
-
* 2.3 Write property tests for SentimentPanel drill-down
- Property 4: Expanded sentiment detail displays all data — Generate random reasoning and citations, render expanded, verify DOM content
- Validates: Requirements 2.2, 2.3
- Property 5: Sentiment detail collapse hides content — Expand then collapse, verify summary visible and detail hidden
- Validates: Requirements 2.4
-
-
3. Checkpoint — Sentiment drill-down complete
- Ensure all tests pass, ask the user if questions arise.
-
4. Fundamentals drill-down — backend
-
4.1 Add
unavailable_fields_jsoncolumn to fundamental model and create Alembic migration- Add
unavailable_fields_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")toapp/models/fundamental.py - Add column to the same Alembic migration as sentiment columns (or a new one if migration 1.1 is already applied), with
server_default='{}' - Requirements: 3.1, 3.2
- Add
-
4.2 Update
FMPFundamentalProviderto record 402 reasons inunavailable_fields- Add
unavailable_fieldsfield toFundamentalDatadataclass - In
_fetch_json_optional, when HTTP 402 is received, map endpoint to field name:ratios-ttm→pe_ratio,financial-growth→revenue_growth,earnings→earnings_surprise - Record
"requires paid plan"as the reason for each affected field - Requirements: 3.1
- Add
-
4.3 Update
fundamental_serviceto persistunavailable_fields- Serialize
unavailable_fieldsdict to JSON string before storing - Requirements: 3.2
- Serialize
-
4.4 Update fundamentals schema and router to include
unavailable_fieldsin API response- Add
unavailable_fields: dict[str, str] = {}toFundamentalResponseinapp/schemas/fundamental.py - Deserialize
unavailable_fields_jsonwhen building the response, catchJSONDecodeErrorand default to{} - Requirements: 3.2
- Add
-
* 4.5 Write property tests for FMP 402 reason recording and round-trip
- Property 6: FMP 402 reason recording — Generate random 402/200 combinations for 3 endpoints, verify unavailable_fields mapping
- Validates: Requirements 3.1
- Property 7: Fundamentals unavailable_fields round-trip — Generate random dicts, store, retrieve, compare
- Validates: Requirements 3.2
-
-
5. Fundamentals drill-down — frontend
-
5.1 Add
unavailable_fieldstoFundamentalResponsetype infrontend/src/lib/types.ts- Requirements: 3.2, 3.3
-
5.2 Update
FundamentalsPanelto show unavailability reasons and expandable detail section- When a metric is null and
unavailable_fields[field_name]exists, display reason text in amber instead of "—" - When a metric is null and no reason exists, display "—"
- Add expand/collapse toggle below summary metrics (default collapsed)
- When expanded: show data source name ("FMP"), fetch timestamp, and list of unavailable fields with reasons
- When collapsed: hide detail, keep summary metrics visible
- Preserve glassmorphism UI style
- Requirements: 3.3, 3.4, 4.1, 4.2, 4.3, 4.4
- When a metric is null and
-
* 5.3 Write property tests for FundamentalsPanel display logic
- Property 8: Null field display depends on reason existence — Generate random FundamentalResponse with various null/reason combos, verify displayed text
- Validates: Requirements 3.3, 3.4
- Property 9: Fundamentals expanded detail content — Generate random response, expand, verify source/timestamp/reasons in DOM
- Validates: Requirements 4.2
-
-
6. Checkpoint — Fundamentals drill-down complete
- Ensure all tests pass, ask the user if questions arise.
-
7. S/R zone clustering — backend
-
7.1 Implement
cluster_sr_zones()function inapp/services/sr_service.py- Sort levels by price ascending
- Greedy merge: walk sorted levels, merge if within tolerance % of current cluster midpoint
- Compute zone: low, high, midpoint, strength (sum capped at 100), level_count
- Tag zone type: "support" if midpoint < current_price, else "resistance"
- Sort by strength descending
- If
max_zonesset, return top N; if 0 or negative, return empty list - Handle empty input by returning empty list
- Requirements: 7.1, 7.2, 7.3, 7.4, 8.2
-
7.2 Add
SRZoneResultschema and updateSRLevelResponseinapp/schemas/sr_level.py- Add
SRZoneResultmodel withlow,high,midpoint,strength,type,level_count - Add
zones: list[SRZoneResult] = []toSRLevelResponse - Requirements: 7.2, 9.1
- Add
-
7.3 Update S/R router to accept
max_zonesparameter and return zones- Add
max_zones: int = 6query parameter to the S/R levels endpoint - Call
cluster_sr_zones()with fetched levels and current price - Include zones in the response
- Requirements: 8.1, 8.3
- Add
-
* 7.4 Write property tests for S/R zone clustering
- Property 14: Clustering merges nearby levels — Generate random level sets and tolerances, verify no two zones have midpoints within tolerance
- Validates: Requirements 7.2
- Property 15: Zone strength is capped sum — Generate random level sets, cluster, verify strength = min(100, sum)
- Validates: Requirements 7.3
- Property 16: Zone type tagging — Generate random zones and current prices, verify support/resistance tagging
- Validates: Requirements 7.4
- Property 17: Zone filtering returns top N by strength — Generate random zone sets and limits, verify top-N selection
- Validates: Requirements 8.2
-
-
8. Checkpoint — S/R clustering backend complete
- Ensure all tests pass, ask the user if questions arise.
-
9. Chart enhancements — zoom and pan
-
9.1 Add
SRZoneandSRLevelResponse.zonestypes tofrontend/src/lib/types.ts- Requirements: 9.1
-
9.2 Implement zoom (mouse wheel) on
CandlestickChart- Add
visibleRange: { start: number, end: number }state initialized to full dataset - Add
onWheelhandler: positive delta narrows range (zoom in), negative widens (zoom out) - Clamp visible range to min 10 bars, max full dataset length
- Disable zoom if dataset has fewer than 10 bars
- Slice data by
visibleRangefor rendering - Debounce zoom via
requestAnimationFrame - Requirements: 5.1, 5.2, 5.3, 5.5
- Add
-
9.3 Implement pan (click-drag) on
CandlestickChart- Add
isPanningandpanStartXstate onMouseDownstarts pan,onMouseMoveshifts visible range proportionally,onMouseUpends pan- Pan only active when zoomed in (visible range < full dataset)
- Clamp range to dataset bounds
- Requirements: 5.4
- Add
-
9.4 Implement crosshair overlay on
CandlestickChart- Add
crosshair: { x: number, y: number } | nullstate onMouseMoveupdates crosshair position- Draw vertical line at cursor x spanning full chart height
- Draw horizontal line at cursor y spanning full chart width
- Display price label on y-axis at horizontal line position
- Display date label on x-axis at vertical line position
onMouseLeaveclears crosshair- Requirements: 6.1, 6.2, 6.3, 6.4, 6.5
- Add
-
* 9.5 Write property tests for chart zoom/pan invariants
- Property 10: Zoom adjusts visible range proportionally — Generate random datasets and wheel deltas, verify range narrows/widens
- Validates: Requirements 5.1, 5.2, 5.3
- Property 11: Pan shifts visible range — Generate random ranges and drag deltas, verify shift without width change
- Validates: Requirements 5.4
- Property 12: Zoom range invariant — Generate random zoom/pan sequences, verify range bounds always valid
- Validates: Requirements 5.5
- Property 13: Coordinate-to-value mapping — Generate random chart configs, verify yToPrice and xToBarIndex mappings
- Validates: Requirements 6.3, 6.4
-
-
10. S/R zone rendering on chart
-
10.1 Update
CandlestickChartto acceptzonesprop and render shaded zone rectangles- Accept
zones?: SRZone[]prop - Render each zone as a semi-transparent rectangle spanning low→high price range across full chart width
- Use green shading (rgba) for support zones, red shading for resistance zones
- Draw zones behind candlestick bodies (render zones first, then candles)
- Display label with midpoint price and strength score for each zone
- Re-render zones correctly at every zoom level using current price scale
- Requirements: 9.1, 9.2, 9.3, 9.4, 9.5
- Accept
-
10.2 Update
SROverlayandTickerDetailPageto pass zones toCandlestickChart- Update
useTickerDetailhook orSROverlayto extract zones from the S/R API response - Pass zones array to
CandlestickChartcomponent - Default to max 6 zones (handled by backend
max_zones=6default) - Requirements: 8.3, 9.1
- Update
-
10.3 Ensure S/R overlays re-render correctly at all zoom levels
- Verify zone rectangles reposition when zoom/pan changes the visible price scale
- Handle single-level zones (low == high) as thin 2px-height rectangles
- Requirements: 5.6, 9.5
-
-
11. Final checkpoint — All features integrated
- Ensure all tests pass, ask the user if questions arise.
Notes
- Tasks marked with
*are optional and can be skipped for faster MVP - Each task references specific requirements for traceability
- Checkpoints ensure incremental validation after each feature area
- Property tests validate universal correctness properties from the design document
- The Alembic migration for sentiment and fundamentals columns should ideally be a single migration file
- S/R zones are computed on-the-fly (not persisted), so no additional migration is needed for zones