# 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 - [x] 1. Sentiment drill-down — backend - [x] 1.1 Add `reasoning` and `citations_json` columns to `SentimentScore` model and create Alembic migration - Add `reasoning: Mapped[str] = mapped_column(Text, nullable=False, default="")` and `citations_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")` to `app/models/sentiment.py` - Create Alembic migration with `server_default` so existing rows are backfilled - _Requirements: 1.1, 1.2, 1.3_ - [x] 1.2 Update `OpenAISentimentProvider` to extract reasoning and citations from OpenAI response - Add `reasoning` and `citations` fields to the `SentimentData` dataclass - Extract `reasoning` from the parsed JSON response body - Iterate `response.output` items for `url_citation` annotations, collect `{"url": ..., "title": ...}` dicts - Return empty citations list when no annotations exist (no error) - _Requirements: 1.1, 1.2, 1.4_ - [x] 1.3 Update `sentiment_service.store_sentiment()` to persist reasoning and citations - Accept `reasoning` and `citations` parameters - Serialize citations to JSON string before storing - _Requirements: 1.3_ - [x] 1.4 Update sentiment schema and router to include reasoning and citations in API response - Add `CitationItem` model and `reasoning`/`citations` fields to `SentimentScoreResult` in `app/schemas/sentiment.py` - Deserialize `citations_json` when building the response, catch `JSONDecodeError` and default to `[]` - _Requirements: 1.3_ - [ ]* 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** - [x] 2. Sentiment drill-down — frontend - [x] 2.1 Add `CitationItem`, `reasoning`, and `citations` fields to `SentimentScore` type in `frontend/src/lib/types.ts` - _Requirements: 1.3, 2.2, 2.3_ - [x] 2.2 Add expandable detail section to `SentimentPanel` - Add `useState(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 `` 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_ - [ ]* 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** - [x] 3. Checkpoint — Sentiment drill-down complete - Ensure all tests pass, ask the user if questions arise. - [x] 4. Fundamentals drill-down — backend - [x] 4.1 Add `unavailable_fields_json` column to fundamental model and create Alembic migration - Add `unavailable_fields_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")` to `app/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_ - [x] 4.2 Update `FMPFundamentalProvider` to record 402 reasons in `unavailable_fields` - Add `unavailable_fields` field to `FundamentalData` dataclass - 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_ - [x] 4.3 Update `fundamental_service` to persist `unavailable_fields` - Serialize `unavailable_fields` dict to JSON string before storing - _Requirements: 3.2_ - [x] 4.4 Update fundamentals schema and router to include `unavailable_fields` in API response - Add `unavailable_fields: dict[str, str] = {}` to `FundamentalResponse` in `app/schemas/fundamental.py` - Deserialize `unavailable_fields_json` when building the response, catch `JSONDecodeError` and default to `{}` - _Requirements: 3.2_ - [ ]* 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** - [x] 5. Fundamentals drill-down — frontend - [x] 5.1 Add `unavailable_fields` to `FundamentalResponse` type in `frontend/src/lib/types.ts` - _Requirements: 3.2, 3.3_ - [x] 5.2 Update `FundamentalsPanel` to 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_ - [ ]* 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** - [x] 6. Checkpoint — Fundamentals drill-down complete - Ensure all tests pass, ask the user if questions arise. - [x] 7. S/R zone clustering — backend - [x] 7.1 Implement `cluster_sr_zones()` function in `app/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_zones` set, 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_ - [x] 7.2 Add `SRZoneResult` schema and update `SRLevelResponse` in `app/schemas/sr_level.py` - Add `SRZoneResult` model with `low`, `high`, `midpoint`, `strength`, `type`, `level_count` - Add `zones: list[SRZoneResult] = []` to `SRLevelResponse` - _Requirements: 7.2, 9.1_ - [x] 7.3 Update S/R router to accept `max_zones` parameter and return zones - Add `max_zones: int = 6` query 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_ - [ ]* 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** - [x] 8. Checkpoint — S/R clustering backend complete - Ensure all tests pass, ask the user if questions arise. - [x] 9. Chart enhancements — zoom and pan - [x] 9.1 Add `SRZone` and `SRLevelResponse.zones` types to `frontend/src/lib/types.ts` - _Requirements: 9.1_ - [x] 9.2 Implement zoom (mouse wheel) on `CandlestickChart` - Add `visibleRange: { start: number, end: number }` state initialized to full dataset - Add `onWheel` handler: 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 `visibleRange` for rendering - Debounce zoom via `requestAnimationFrame` - _Requirements: 5.1, 5.2, 5.3, 5.5_ - [x] 9.3 Implement pan (click-drag) on `CandlestickChart` - Add `isPanning` and `panStartX` state - `onMouseDown` starts pan, `onMouseMove` shifts visible range proportionally, `onMouseUp` ends pan - Pan only active when zoomed in (visible range < full dataset) - Clamp range to dataset bounds - _Requirements: 5.4_ - [x] 9.4 Implement crosshair overlay on `CandlestickChart` - Add `crosshair: { x: number, y: number } | null` state - `onMouseMove` updates 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 - `onMouseLeave` clears crosshair - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_ - [ ]* 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** - [x] 10. S/R zone rendering on chart - [x] 10.1 Update `CandlestickChart` to accept `zones` prop 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_ - [x] 10.2 Update `SROverlay` and `TickerDetailPage` to pass zones to `CandlestickChart` - Update `useTickerDetail` hook or `SROverlay` to extract zones from the S/R API response - Pass zones array to `CandlestickChart` component - Default to max 6 zones (handled by backend `max_zones=6` default) - _Requirements: 8.3, 9.1_ - [x] 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_ - [x] 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