major update
This commit is contained in:
351
.kiro/specs/score-transparency-trade-overlay/design.md
Normal file
351
.kiro/specs/score-transparency-trade-overlay/design.md
Normal 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
|
||||
Reference in New Issue
Block a user