major update
Some checks failed
Deploy / lint (push) Failing after 8s
Deploy / test (push) Has been skipped
Deploy / deploy (push) Has been skipped

This commit is contained in:
Dennis Thiessen
2026-02-27 16:08:09 +01:00
parent 61ab24490d
commit 181cfe6588
71 changed files with 7647 additions and 281 deletions

View File

@@ -0,0 +1,351 @@
# 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