# Design Document: Score Transparency & Trade Overlay

## Overview

This feature extends the stock signal platform in two areas:

1. **Score Transparency** — The scoring API and UI are enhanced to expose the full breakdown of how each dimension score and the composite score are calculated. Each dimension returns its sub-scores, raw input values, weights, and formula descriptions. The frontend renders expandable panels showing this detail.

2. **Trade Setup Chart Overlay** — When a trade setup exists for a ticker (from the R:R scanner), the candlestick chart renders colored zones for entry, stop-loss, and take-profit levels. The ticker detail page fetches trade data and passes it to the chart.

Both features are additive — they extend existing API responses and UI components without breaking current behavior.

## Architecture

The changes follow the existing layered architecture:

```
┌─────────────────────────────────────────────────────┐
│                  Frontend (React)                    │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────┐  │
│  │  ScoreCard   │  │ Dimension    │  │Candlestick│  │
│  │  (composite  │  │ Panel        │  │Chart      │  │
│  │   weights)   │  │ (breakdowns) │  │(trade     │  │
│  └──────┬───────┘  └──────┬───────┘  │ overlay)  │  │
│         │                 │          └─────┬─────┘  │
│  ┌──────┴─────────────────┴────────────────┴─────┐  │
│  │         useTickerDetail + useTrades            │  │
│  └──────────────────┬────────────────────────────┘  │
└─────────────────────┼───────────────────────────────┘
                      │ HTTP
┌─────────────────────┼───────────────────────────────┐
│                  Backend (FastAPI)                    │
│  ┌──────────────────┴────────────────────────────┐  │
│  │              scores router                     │  │
│  │  GET /api/v1/scores/{symbol}                   │  │
│  │  (extended response with breakdowns)           │  │
│  └──────────────────┬────────────────────────────┘  │
│  ┌──────────────────┴────────────────────────────┐  │
│  │           scoring_service.py                   │  │
│  │  _compute_*_score → returns ScoreBreakdown     │  │
│  │  get_score → assembles full breakdown response │  │
│  └───────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘
```

Key design decisions:
- **Backend-driven breakdowns**: Each `_compute_*_score` function is refactored to return a `ScoreBreakdown` dict alongside the numeric score, rather than computing breakdowns separately. This ensures the breakdown always matches the actual score.
- **Single API call**: The existing `GET /api/v1/scores/{symbol}` endpoint is extended (not a new endpoint) to include breakdowns in the response. This avoids extra round-trips.
- **Trade overlay via props**: The `CandlestickChart` component receives an optional `tradeSetup` prop. The chart draws overlay elements using the existing canvas rendering pipeline — no new library needed.
- **Trade data reuse**: The frontend reuses the existing `useTrades` hook and trades API. The `TickerDetailPage` filters for the current symbol client-side.

## Components and Interfaces

### Backend

#### Modified: `scoring_service.py` — Dimension compute functions

Each `_compute_*_score` function changes from returning `float | None` to returning a tuple `(float | None, ScoreBreakdown | None)` where `ScoreBreakdown` is a typed dict:

```python
class SubScoreDetail(TypedDict):
    name: str
    score: float
    weight: float
    raw_value: float | str | None
    description: str

class ScoreBreakdown(TypedDict):
    sub_scores: list[SubScoreDetail]
    formula: str
    unavailable: list[dict[str, str]]  # [{"name": ..., "reason": ...}]
```

- `_compute_technical_score` → returns sub-scores for ADX (0.4), EMA (0.3), RSI (0.3) with raw indicator values
- `_compute_sentiment_score` → returns record count, decay rate, lookback window, weighted average formula
- `_compute_fundamental_score` → returns PE Ratio, Revenue Growth, Earnings Surprise sub-scores with raw values
- `_compute_momentum_score` → returns 5-day ROC (0.5), 20-day ROC (0.5) with raw percentages
- `_compute_sr_quality_score` → returns strong count (max 40), proximity (max 30), avg strength (max 30) with inputs

#### Modified: `scoring_service.py` — `get_score`

Assembles the full response including breakdowns per dimension and composite weight info (available vs missing dimensions, re-normalized weights).

#### Modified: `app/schemas/score.py`

New Pydantic models:

```python
class SubScoreResponse(BaseModel):
    name: str
    score: float
    weight: float
    raw_value: float | str | None = None
    description: str = ""

class ScoreBreakdownResponse(BaseModel):
    sub_scores: list[SubScoreResponse]
    formula: str
    unavailable: list[dict[str, str]] = []

class DimensionScoreResponse(BaseModel):  # extended
    dimension: str
    score: float
    is_stale: bool
    computed_at: datetime | None = None
    breakdown: ScoreBreakdownResponse | None = None  # NEW

class CompositeBreakdownResponse(BaseModel):
    weights: dict[str, float]
    available_dimensions: list[str]
    missing_dimensions: list[str]
    renormalized_weights: dict[str, float]
    formula: str

class ScoreResponse(BaseModel):  # extended
    # ... existing fields ...
    composite_breakdown: CompositeBreakdownResponse | None = None  # NEW
```

#### Modified: `app/routers/scores.py`

The `read_score` endpoint populates the new breakdown fields from the service response.

### Frontend

#### Modified: `frontend/src/lib/types.ts`

New TypeScript types:

```typescript
interface SubScore {
  name: string;
  score: number;
  weight: number;
  raw_value: number | string | null;
  description: string;
}

interface ScoreBreakdown {
  sub_scores: SubScore[];
  formula: string;
  unavailable: { name: string; reason: string }[];
}

interface CompositeBreakdown {
  weights: Record<string, number>;
  available_dimensions: string[];
  missing_dimensions: string[];
  renormalized_weights: Record<string, number>;
  formula: string;
}
```

Extended existing types:
- `DimensionScoreDetail` gains `breakdown?: ScoreBreakdown`
- `ScoreResponse` gains `composite_breakdown?: CompositeBreakdown`

#### New: `frontend/src/components/ticker/DimensionBreakdownPanel.tsx`

An expandable panel component that renders inside the ScoreCard for each dimension. Shows:
- Chevron toggle for expand/collapse
- Sub-score rows: name, bar visualization, score value, weight badge, raw input value
- Formula description text
- Muted "unavailable" labels for missing sub-scores

#### Modified: `frontend/src/components/ui/ScoreCard.tsx`

- Each dimension row becomes clickable/expandable, rendering `DimensionBreakdownPanel` when expanded
- Composite score section shows dimension weights next to each bar
- Missing dimensions shown with muted styling and "redistributed" indicator
- Tooltip/inline text explaining weighted average with re-normalization

#### Modified: `frontend/src/components/charts/CandlestickChart.tsx`

New optional prop: `tradeSetup?: TradeSetup`

When provided, the chart draws:
- Entry price: dashed horizontal line (blue/white) spanning full width
- Stop-loss zone: red semi-transparent rectangle between entry and stop-loss
- Take-profit zone: green semi-transparent rectangle between entry and target
- Price labels on y-axis for entry, stop, target
- All three price levels included in y-axis range calculation
- Hover tooltip showing direction, entry, stop, target, R:R ratio

#### Modified: `frontend/src/pages/TickerDetailPage.tsx`

- Calls `useTrades()` to fetch all trade setups
- Filters for current symbol, picks latest by `detected_at`
- Passes `tradeSetup` prop to `CandlestickChart`
- Renders a trade setup summary card below the chart when a setup exists
- Handles trades API failure gracefully (chart renders without overlay, error logged)

#### Modified: `frontend/src/hooks/useTickerDetail.ts`

Adds trades query to the hook return value so the page has access to trade data.

## Data Models

### Backend Schema Changes

No new database tables. The breakdown data is computed on-the-fly from existing data and returned in the API response only.

### API Response Shape (extended `GET /api/v1/scores/{symbol}`)

```json
{
  "status": "success",
  "data": {
    "symbol": "AAPL",
    "composite_score": 72.5,
    "composite_stale": false,
    "weights": { "technical": 0.25, "sr_quality": 0.20, "sentiment": 0.15, "fundamental": 0.20, "momentum": 0.20 },
    "composite_breakdown": {
      "weights": { "technical": 0.25, "sr_quality": 0.20, "sentiment": 0.15, "fundamental": 0.20, "momentum": 0.20 },
      "available_dimensions": ["technical", "sr_quality", "fundamental", "momentum"],
      "missing_dimensions": ["sentiment"],
      "renormalized_weights": { "technical": 0.294, "sr_quality": 0.235, "fundamental": 0.235, "momentum": 0.235 },
      "formula": "Weighted average of available dimensions with re-normalized weights: sum(weight_i * score_i) / sum(weight_i)"
    },
    "dimensions": [
      {
        "dimension": "technical",
        "score": 68.2,
        "is_stale": false,
        "computed_at": "2024-01-15T10:30:00Z",
        "breakdown": {
          "sub_scores": [
            { "name": "ADX", "score": 72.0, "weight": 0.4, "raw_value": 72.0, "description": "ADX value (0-100). Higher = stronger trend." },
            { "name": "EMA", "score": 65.0, "weight": 0.3, "raw_value": 1.5, "description": "Price 1.5% above EMA(20). Score: 50 + pct_diff * 10." },
            { "name": "RSI", "score": 62.0, "weight": 0.3, "raw_value": 62.0, "description": "RSI(14) value. Score equals RSI." }
          ],
          "formula": "Weighted average: 0.4*ADX + 0.3*EMA + 0.3*RSI, re-normalized if any sub-score unavailable.",
          "unavailable": []
        }
      }
    ],
    "missing_dimensions": ["sentiment"],
    "computed_at": "2024-01-15T10:30:00Z"
  }
}
```

### Trade Setup Data (existing, no changes)

The `TradeSetup` type already exists in `frontend/src/lib/types.ts` with all needed fields: `symbol`, `direction`, `entry_price`, `stop_loss`, `target`, `rr_ratio`, `detected_at`.


## Correctness Properties

*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*

### Property 1: Dimension breakdown contains correct sub-scores

*For any* dimension type (technical, sentiment, fundamental, momentum, sr_quality) and any valid input data sufficient to compute that dimension, the returned `ScoreBreakdown` shall contain exactly the expected sub-score names with the correct weights for that dimension type, and each sub-score's `raw_value` shall be non-null.

Specifically:
- technical → ADX (0.4), EMA (0.3), RSI (0.3)
- sentiment → record_count, decay_rate, lookback_window as sub-score metadata
- fundamental → PE Ratio, Revenue Growth, Earnings Surprise (equal weight)
- momentum → 5-day ROC (0.5), 20-day ROC (0.5)
- sr_quality → Strong Count (max 40), Proximity (max 30), Avg Strength (max 30)

**Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5, 1.6**

### Property 2: Composite re-normalization correctness

*For any* set of dimension scores where at least one dimension is available and zero or more are missing, the composite breakdown shall:
- List exactly the available dimensions in `available_dimensions`
- List exactly the missing dimensions in `missing_dimensions`
- Have `renormalized_weights` that sum to 1.0 (within floating-point tolerance)
- Have each renormalized weight equal to `original_weight / sum(available_original_weights)`

**Validates: Requirements 1.7, 3.2**

### Property 3: Dimension breakdown UI rendering completeness

*For any* `ScoreBreakdown` object with N sub-scores, the `DimensionBreakdownPanel` component shall render exactly N sub-score rows, each containing the sub-score name, numeric score value, weight, and raw input value.

**Validates: Requirements 2.1**

### Property 4: Composite weight display

*For any* score response with K dimensions (some available, some missing), the `ScoreCard` component shall render the weight value next to each dimension bar, and missing dimensions shall be rendered with a visually distinct (muted/dimmed) style.

**Validates: Requirements 3.1, 3.2**

### Property 5: Trade overlay y-axis range includes all trade levels

*For any* OHLCV dataset and any `TradeSetup` (with entry_price, stop_loss, target), the chart's computed y-axis range `[lo, hi]` shall satisfy: `lo <= min(entry_price, stop_loss, target)` and `hi >= max(entry_price, stop_loss, target)`.

**Validates: Requirements 4.4**

### Property 6: Trade setup selection picks latest matching symbol

*For any* non-empty list of `TradeSetup` objects and any symbol string, filtering for that symbol and selecting by latest `detected_at` shall return the setup with the maximum `detected_at` among all setups matching that symbol. If no setups match, the result shall be null/undefined.

**Validates: Requirements 5.1, 5.5**

## Error Handling

| Scenario | Behavior |
|---|---|
| Dimension computation fails (insufficient data) | Score returns `None`, breakdown includes unavailable sub-scores with reason strings (Req 1.8) |
| Individual sub-score fails (e.g., ADX needs 28 bars but only 20 available) | Sub-score omitted from breakdown, added to `unavailable` list with reason. Remaining sub-scores re-normalized (Req 1.8) |
| Trades API request fails on TickerDetailPage | Chart renders without trade overlay. Error logged to console. Page remains functional (Req 5.4) |
| No trade setup exists for current symbol | Chart renders normally without any overlay elements (Req 4.6) |
| Score breakdown data is null (stale or never computed) | DimensionPanel shows score without expandable breakdown section |
| Composite has zero available dimensions | `composite_score` is `null`, `composite_breakdown` shows all dimensions as missing |

## Testing Strategy

### Unit Tests

Unit tests cover specific examples and edge cases:

- **Backend**: Test each `_compute_*_score` function returns correct breakdown structure for known input data. Test edge cases: missing sub-scores, all sub-scores missing, single sub-score available.
- **Frontend components**: Test `DimensionBreakdownPanel` renders correctly for each dimension type with known breakdown data. Test expand/collapse behavior. Test unavailable sub-score rendering.
- **Trade overlay**: Test `CandlestickChart` draws overlay elements when `tradeSetup` prop is provided. Test no overlay when prop is absent. Test tooltip content on hover.
- **Trade setup selection**: Test filtering and latest-selection logic with specific examples including edge cases (no matches, single match, multiple matches with same timestamp).
- **Composite display**: Test `ScoreCard` renders weights, missing dimension indicators, and re-normalization explanation.

### Property-Based Tests

Property-based tests use `hypothesis` (Python backend) and `fast-check` (TypeScript frontend) with minimum 100 iterations per property.

Each property test references its design document property:

- **Property 1** — `Feature: score-transparency-trade-overlay, Property 1: Dimension breakdown contains correct sub-scores`
  Generate random valid indicator data for each dimension type, compute the score, and verify the breakdown structure matches the expected sub-score names and weights.

- **Property 2** — `Feature: score-transparency-trade-overlay, Property 2: Composite re-normalization correctness`
  Generate random subsets of 5 dimensions (1-5 available), assign random weights, compute re-normalized weights, and verify they sum to 1.0 and each equals `original / sum(available)`.

- **Property 3** — `Feature: score-transparency-trade-overlay, Property 3: Dimension breakdown UI rendering completeness`
  Generate random `ScoreBreakdown` objects with 1-5 sub-scores, render `DimensionBreakdownPanel`, and verify the DOM contains exactly N sub-score rows with all required fields.

- **Property 4** — `Feature: score-transparency-trade-overlay, Property 4: Composite weight display`
  Generate random score responses with random available/missing dimension combinations, render `ScoreCard`, and verify weight labels are present and missing dimensions are visually distinct.

- **Property 5** — `Feature: score-transparency-trade-overlay, Property 5: Trade overlay y-axis range includes all trade levels`
  Generate random OHLCV data and random trade setups, compute the chart y-axis range, and verify all three trade levels fall within `[lo, hi]`.

- **Property 6** — `Feature: score-transparency-trade-overlay, Property 6: Trade setup selection picks latest matching symbol`
  Generate random lists of trade setups with random symbols and timestamps, apply the selection logic, and verify the result is the latest setup for the target symbol.

### Test Configuration

- Python property tests: `hypothesis` library, `@settings(max_examples=100)`
- TypeScript property tests: `fast-check` library, `fc.assert(property, { numRuns: 100 })`
- Each property test tagged with a comment: `Feature: score-transparency-trade-overlay, Property N: <title>`
- Each correctness property implemented by a single property-based test
