diff --git a/.env.example b/.env.example
index e6cee4c..ccc4a0e 100644
--- a/.env.example
+++ b/.env.example
@@ -13,14 +13,27 @@ ALPACA_API_SECRET=
GEMINI_API_KEY=
GEMINI_MODEL=gemini-2.0-flash
+# Sentiment Provider — OpenAI
+OPENAI_API_KEY=
+OPENAI_MODEL=gpt-4o-mini
+OPENAI_SENTIMENT_BATCH_SIZE=5
+
# Fundamentals Provider — Financial Modeling Prep
FMP_API_KEY=
+# Fundamentals Provider — Finnhub (optional fallback)
+FINNHUB_API_KEY=
+
+# Fundamentals Provider — Alpha Vantage (optional fallback)
+ALPHA_VANTAGE_API_KEY=
+
# Scheduled Jobs
DATA_COLLECTOR_FREQUENCY=daily
SENTIMENT_POLL_INTERVAL_MINUTES=30
FUNDAMENTAL_FETCH_FREQUENCY=daily
RR_SCAN_FREQUENCY=daily
+FUNDAMENTAL_RATE_LIMIT_RETRIES=3
+FUNDAMENTAL_RATE_LIMIT_BACKOFF_SECONDS=15
# Scoring Defaults
DEFAULT_WATCHLIST_AUTO_SIZE=10
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index ac51152..3d0c326 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -40,6 +40,9 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "20"
- run: pip install -e ".[dev]"
- run: alembic upgrade head
env:
@@ -47,6 +50,15 @@ jobs:
- run: pytest --tb=short
env:
DATABASE_URL: postgresql+asyncpg://test_user:test_pass@localhost:5432/test_db
+ - run: |
+ cd frontend
+ npm ci
+ if node -e "require.resolve('vitest/package.json')" >/dev/null 2>&1; then
+ npm test
+ else
+ echo "vitest not configured; skipping frontend tests"
+ fi
+ npm run build
deploy:
needs: test
@@ -65,4 +77,8 @@ jobs:
source .venv/bin/activate
pip install -e .
alembic upgrade head
+ cd frontend
+ npm ci
+ npm run build
+ cd ..
sudo systemctl restart stock-data-backend
diff --git a/.kiro/hooks/code-quality-analyzer.kiro.hook b/.kiro/hooks/code-quality-analyzer.kiro.hook
new file mode 100644
index 0000000..2888151
--- /dev/null
+++ b/.kiro/hooks/code-quality-analyzer.kiro.hook
@@ -0,0 +1,27 @@
+{
+ "enabled": true,
+ "name": "Code Quality Analyzer",
+ "description": "Analyzes modified source code files for potential improvements including code smells, design patterns, best practices, readability, maintainability, and performance optimizations",
+ "version": "1",
+ "when": {
+ "type": "fileEdited",
+ "patterns": [
+ "*.py",
+ "*.ts",
+ "*.tsx",
+ "*.js",
+ "*.jsx",
+ "*.java",
+ "*.go",
+ "*.rs",
+ "*.cpp",
+ "*.c",
+ "*.h",
+ "*.cs"
+ ]
+ },
+ "then": {
+ "type": "askAgent",
+ "prompt": "Analyze the modified code for potential improvements. Check for: 1) Code smells and anti-patterns, 2) Opportunities to apply design patterns, 3) Best practices violations, 4) Readability improvements, 5) Maintainability concerns, 6) Performance optimization opportunities. Provide specific, actionable suggestions while ensuring functionality remains intact."
+ }
+}
\ No newline at end of file
diff --git a/.kiro/hooks/update-docs-on-change.kiro.hook b/.kiro/hooks/update-docs-on-change.kiro.hook
new file mode 100644
index 0000000..cbb4200
--- /dev/null
+++ b/.kiro/hooks/update-docs-on-change.kiro.hook
@@ -0,0 +1,19 @@
+{
+ "enabled": true,
+ "name": "Update Docs on Code Change",
+ "description": "Monitors Python source files and prompts agent to update README.md or docs folder when code changes are saved",
+ "version": "1",
+ "when": {
+ "type": "fileEdited",
+ "patterns": [
+ "*.py",
+ "requirements.txt",
+ "pyproject.toml",
+ "alembic.ini"
+ ]
+ },
+ "then": {
+ "type": "askAgent",
+ "prompt": "A source file was just modified. Review the changes and update the documentation in README.md to reflect any new features, API changes, configuration updates, or important implementation details. Keep the documentation clear, accurate, and up-to-date."
+ }
+}
\ No newline at end of file
diff --git a/.kiro/settings/mcp.json b/.kiro/settings/mcp.json
index 991f52d..1f4957f 100644
--- a/.kiro/settings/mcp.json
+++ b/.kiro/settings/mcp.json
@@ -1,80 +1,5 @@
{
"mcpServers": {
- "context7": {
- "gallery": true,
- "command": "npx",
- "args": [
- "-y",
- "@upstash/context7-mcp@latest"
- ],
- "env": {
- "HTTP_PROXY": "http://aproxy.corproot.net:8080",
- "HTTPS_PROXY": "http://aproxy.corproot.net:8080"
- },
- "type": "stdio"
- },
- "aws.mcp": {
- "command": "uvx",
- "timeout": 100000,
- "transport": "stdio",
- "args": [
- "mcp-proxy-for-aws@latest",
- "https://aws-mcp.us-east-1.api.aws/mcp"
- ],
- "env": {
- "AWS_PROFILE": "409330224121_sc-ps-standard-admin",
- "AWS_REGION": "eu-central-2",
- "HTTP_PROXY": "http://aproxy.corproot.net:8080",
- "HTTPS_PROXY": "http://aproxy.corproot.net:8080",
- "SSL_CERT_FILE": "/Users/taathde3/combined-ca-bundle.pem",
- "REQUESTS_CA_BUNDLE": "/Users/taathde3/combined-ca-bundle.pem"
-
- },
- "disabled": false,
- "autoApprove": []
- },
- "aws.eks.mcp": {
- "command": "uvx",
- "timeout": 100000,
- "transport": "stdio",
- "args": [
- "mcp-proxy-for-aws@latest",
- "https://eks-mcp.eu-central-1.api.aws/mcp",
- "--service",
- "eks-mcp"
- ],
- "env": {
- "AWS_PROFILE": "409330224121_sc-ps-standard-admin",
- "AWS_REGION": "eu-central-2",
- "HTTP_PROXY": "http://aproxy.corproot.net:8080",
- "HTTPS_PROXY": "http://aproxy.corproot.net:8080",
- "SSL_CERT_FILE": "/Users/taathde3/combined-ca-bundle.pem",
- "REQUESTS_CA_BUNDLE": "/Users/taathde3/combined-ca-bundle.pem"
- },
- "disabled": false,
- "autoApprove": []
- },
- "aws.ecs.mcp": {
- "command": "uvx",
- "timeout": 100000,
- "transport": "stdio",
- "args": [
- "mcp-proxy-for-aws@latest",
- "https://ecs-mcp.us-east-1.api.aws/mcp",
- "--service",
- "ecs-mcp"
- ],
- "env": {
- "AWS_PROFILE": "409330224121_sc-ps-standard-admin",
- "AWS_REGION": "eu-central-2",
- "HTTP_PROXY": "http://aproxy.corproot.net:8080",
- "HTTPS_PROXY": "http://aproxy.corproot.net:8080",
- "SSL_CERT_FILE": "/Users/taathde3/combined-ca-bundle.pem",
- "REQUESTS_CA_BUNDLE": "/Users/taathde3/combined-ca-bundle.pem"
- },
- "disabled": false,
- "autoApprove": []
- },
"iaws.support.agent": {
"command": "uvx",
"args": [
diff --git a/.kiro/specs/intelligent-trade-recommendations/.config.kiro b/.kiro/specs/intelligent-trade-recommendations/.config.kiro
new file mode 100644
index 0000000..9f9ba88
--- /dev/null
+++ b/.kiro/specs/intelligent-trade-recommendations/.config.kiro
@@ -0,0 +1 @@
+{"specId": "71b6e4c6-56fa-4d43-b1ca-c4a89e8c8b5e", "workflowType": "requirements-first", "specType": "feature"}
diff --git a/.kiro/specs/intelligent-trade-recommendations/design.md b/.kiro/specs/intelligent-trade-recommendations/design.md
new file mode 100644
index 0000000..3e76603
--- /dev/null
+++ b/.kiro/specs/intelligent-trade-recommendations/design.md
@@ -0,0 +1,2079 @@
+# Design Document: Intelligent Trade Recommendation System
+
+## Overview
+
+The Intelligent Trade Recommendation System enhances the Signal Dashboard's trade setup generation by providing bidirectional analysis (LONG and SHORT), confidence scoring, multiple price targets with probability estimates, and signal conflict detection. This system transforms raw multi-dimensional signals into actionable trading recommendations suitable for non-professional traders.
+
+### Goals
+
+- Generate both LONG and SHORT trade setups for every ticker with independent confidence scores
+- Identify 3-5 price targets at S/R levels with probability estimates for staged profit-taking
+- Detect and flag contradictions between sentiment, technical, momentum, and fundamental signals
+- Provide clear recommendation summaries with action, reasoning, and risk level
+- Maintain performance targets: 500ms per ticker, 10 tickers/second batch processing
+
+### Non-Goals
+
+- Real-time trade execution or order management
+- Backtesting or historical performance tracking (deferred to future phase)
+- Machine learning-based prediction models
+- Integration with external trading platforms
+
+### Key Design Decisions
+
+1. **Extend TradeSetup model** rather than create new tables to maintain backward compatibility
+2. **Synchronous recommendation generation** during R:R scanner job (no separate scheduled job)
+3. **Quality-score based target selection** combining R:R ratio, S/R strength, and proximity
+4. **Rule-based confidence scoring** using dimension score thresholds and alignment checks
+5. **JSON fields for flexible data** (targets array, conflict flags) to avoid complex schema changes
+
+
+
+## Architecture
+
+### System Context
+
+```mermaid
+graph TB
+ subgraph "Existing System"
+ RR[R:R Scanner Service]
+ SCORE[Scoring Service]
+ SR[S/R Service]
+ IND[Indicator Service]
+ SENT[Sentiment Service]
+ FUND[Fundamental Service]
+ end
+
+ subgraph "New Components"
+ REC[Recommendation Engine]
+ DIR[Direction Analyzer]
+ TGT[Target Generator]
+ PROB[Probability Estimator]
+ CONF[Signal Conflict Detector]
+ end
+
+ subgraph "Data Layer"
+ TS[(TradeSetup Model)]
+ DS[(DimensionScore)]
+ SRL[(SRLevel)]
+ SENT_M[(SentimentScore)]
+ end
+
+ RR --> REC
+ SCORE --> REC
+ SR --> REC
+
+ REC --> DIR
+ REC --> TGT
+ REC --> PROB
+ REC --> CONF
+
+ DIR --> TS
+ TGT --> TS
+ PROB --> TS
+ CONF --> TS
+
+ DS --> DIR
+ DS --> CONF
+ SRL --> TGT
+ SRL --> PROB
+ SENT_M --> CONF
+```
+
+### Integration Strategy
+
+The recommendation system integrates into the existing R:R scanner workflow:
+
+1. **Trigger Point**: `rr_scanner_service.scan_ticker()` generates base LONG/SHORT setups
+2. **Enhancement Phase**: New `recommendation_service.enhance_trade_setup()` enriches each setup
+3. **Persistence**: Extended TradeSetup model stores all recommendation data
+4. **API Layer**: Existing `/api/v1/trades` endpoints return enhanced data
+
+This approach ensures:
+- Zero breaking changes to existing scanner logic
+- Backward compatibility with current TradeSetup consumers
+- Single transaction for setup generation and enhancement
+- No additional scheduled jobs required
+
+
+
+## Components and Interfaces
+
+### Recommendation Engine (recommendation_service.py)
+
+**Responsibility**: Orchestrate the recommendation generation process for a trade setup.
+
+**Interface**:
+```python
+async def enhance_trade_setup(
+ db: AsyncSession,
+ ticker: Ticker,
+ setup: TradeSetup,
+ dimension_scores: dict[str, float],
+ sr_levels: list[SRLevel],
+ sentiment_classification: str | None,
+ atr_value: float,
+) -> TradeSetup:
+ """Enhance a base trade setup with recommendation data.
+
+ Args:
+ db: Database session
+ ticker: Ticker model instance
+ setup: Base TradeSetup with direction, entry, stop, target, rr_ratio
+ dimension_scores: Dict of dimension -> score (technical, sentiment, momentum, etc.)
+ sr_levels: All S/R levels for the ticker
+ sentiment_classification: Latest sentiment (bearish/neutral/bullish)
+ atr_value: Current ATR for volatility adjustment
+
+ Returns:
+ Enhanced TradeSetup with confidence_score, targets, conflict_flags, etc.
+ """
+```
+
+**Algorithm**:
+1. Call `direction_analyzer.calculate_confidence()` to get confidence score
+2. Call `target_generator.generate_targets()` to get 3-5 targets
+3. Call `probability_estimator.estimate_probabilities()` for each target
+4. Call `signal_conflict_detector.detect_conflicts()` to identify contradictions
+5. Generate recommendation summary based on confidence and conflicts
+6. Update setup model with all recommendation data
+7. Return enhanced setup
+
+**Dependencies**: All four sub-components, SystemSetting for thresholds
+
+
+
+### Direction Analyzer
+
+**Responsibility**: Calculate confidence scores for LONG and SHORT directions based on signal alignment.
+
+**Interface**:
+```python
+def calculate_confidence(
+ direction: str,
+ dimension_scores: dict[str, float],
+ sentiment_classification: str | None,
+) -> float:
+ """Calculate confidence score (0-100%) for a trade direction.
+
+ Args:
+ direction: "long" or "short"
+ dimension_scores: Dict with keys: technical, sentiment, momentum, fundamental
+ sentiment_classification: "bearish", "neutral", "bullish", or None
+
+ Returns:
+ Confidence score 0-100%
+ """
+```
+
+**Algorithm**:
+```
+Base confidence = 50.0
+
+For LONG direction:
+ - If technical > 60: add 15 points
+ - If technical > 70: add additional 10 points
+ - If momentum > 60: add 15 points
+ - If sentiment is "bullish": add 15 points
+ - If fundamental > 60: add 10 points
+
+For SHORT direction:
+ - If technical < 40: add 15 points
+ - If technical < 30: add additional 10 points
+ - If momentum < 40: add 15 points
+ - If sentiment is "bearish": add 15 points
+ - If fundamental < 40: add 10 points
+
+Clamp result to [0, 100]
+```
+
+**Rationale**: Rule-based scoring provides transparency and predictability. Weights favor technical and momentum (15 points each) as they reflect price action, with sentiment and fundamentals as supporting factors.
+
+
+
+### Target Generator
+
+**Responsibility**: Identify 3-5 price targets at S/R levels with classification and R:R calculation.
+
+**Interface**:
+```python
+def generate_targets(
+ direction: str,
+ entry_price: float,
+ stop_loss: float,
+ sr_levels: list[SRLevel],
+ atr_value: float,
+) -> list[dict]:
+ """Generate multiple price targets for a trade setup.
+
+ Args:
+ direction: "long" or "short"
+ entry_price: Entry price for the trade
+ stop_loss: Stop loss price
+ sr_levels: All S/R levels for the ticker
+ atr_value: Current ATR value
+
+ Returns:
+ List of target dicts with keys:
+ - price: Target price level
+ - distance_from_entry: Absolute distance
+ - distance_atr_multiple: Distance as multiple of ATR
+ - rr_ratio: Risk-reward ratio for this target
+ - classification: "Conservative", "Moderate", or "Aggressive"
+ - sr_level_id: ID of the S/R level used
+ - sr_strength: Strength score of the S/R level
+ """
+```
+
+**Algorithm**:
+```
+1. Filter S/R levels by direction:
+ - LONG: resistance levels above entry (type="resistance", price > entry)
+ - SHORT: support levels below entry (type="support", price < entry)
+
+2. Apply volatility filter:
+ - Exclude levels within 1x ATR of entry (too close)
+ - If ATR > 5% of price: include levels up to 10x ATR
+ - If ATR < 2% of price: limit to levels within 3x ATR
+
+3. Calculate quality score for each candidate:
+ quality = 0.35 * norm_rr + 0.35 * norm_strength + 0.30 * norm_proximity
+ where:
+ - norm_rr = min(rr_ratio / 10.0, 1.0)
+ - norm_strength = strength / 100.0
+ - norm_proximity = 1.0 - min(distance / entry, 1.0)
+
+4. Sort candidates by quality score descending
+
+5. Select top 3-5 targets:
+ - Take top 5 if available
+ - Minimum 3 required (flag setup if fewer)
+
+6. Classify targets by distance:
+ - Conservative: nearest 1-2 targets
+ - Aggressive: furthest 1-2 targets
+ - Moderate: middle targets
+
+7. Calculate R:R ratio for each target:
+ risk = abs(entry_price - stop_loss)
+ reward = abs(target_price - entry_price)
+ rr_ratio = reward / risk
+```
+
+**Rationale**: Quality-based selection ensures targets balance multiple factors. ATR-based filtering adapts to volatility. Classification helps traders plan staged exits.
+
+
+
+### Probability Estimator
+
+**Responsibility**: Calculate probability (0-100%) of reaching each price target.
+
+**Interface**:
+```python
+def estimate_probability(
+ target: dict,
+ dimension_scores: dict[str, float],
+ sentiment_classification: str | None,
+ direction: str,
+ config: dict,
+) -> float:
+ """Estimate probability of reaching a price target.
+
+ Args:
+ target: Target dict from generate_targets()
+ dimension_scores: Current dimension scores
+ sentiment_classification: Latest sentiment
+ direction: "long" or "short"
+ config: Configuration dict with weights from SystemSetting
+
+ Returns:
+ Probability percentage 0-100%
+ """
+```
+
+**Algorithm**:
+```
+Base probability calculation:
+
+1. Distance factor (40% weight):
+ - Conservative targets (nearest): base = 70%
+ - Moderate targets (middle): base = 55%
+ - Aggressive targets (furthest): base = 40%
+
+2. S/R strength factor (30% weight):
+ - strength >= 80: add 15%
+ - strength 60-79: add 10%
+ - strength 40-59: add 5%
+ - strength < 40: subtract 10%
+
+3. Signal alignment factor (20% weight):
+ - Check if signals support direction:
+ * LONG: technical > 60 AND (sentiment bullish OR momentum > 60)
+ * SHORT: technical < 40 AND (sentiment bearish OR momentum < 40)
+ - If aligned: add 15%
+ - If conflicted: subtract 15%
+
+4. Volatility factor (10% weight):
+ - If distance_atr_multiple > 5: add 5% (high volatility favors distant targets)
+ - If distance_atr_multiple < 2: add 5% (low volatility favors near targets)
+
+Final probability = base + strength_adj + alignment_adj + volatility_adj
+Clamp to [10, 90] (never 0% or 100% to reflect uncertainty)
+```
+
+**Configuration Parameters** (stored in SystemSetting):
+- `signal_alignment_weight`: Default 0.15 (15%)
+- `sr_strength_weight`: Default 0.20 (20%)
+- `distance_penalty_factor`: Default 0.10 (10%)
+
+**Rationale**: Multi-factor approach balances distance (primary), S/R quality (secondary), and signal confirmation (tertiary). Clamping to [10, 90] acknowledges market uncertainty.
+
+
+
+### Signal Conflict Detector
+
+**Responsibility**: Identify contradictions between sentiment, technical, momentum, and fundamental signals.
+
+**Interface**:
+```python
+def detect_conflicts(
+ dimension_scores: dict[str, float],
+ sentiment_classification: str | None,
+) -> list[str]:
+ """Detect signal conflicts across dimensions.
+
+ Args:
+ dimension_scores: Dict with technical, sentiment, momentum, fundamental scores
+ sentiment_classification: "bearish", "neutral", "bullish", or None
+
+ Returns:
+ List of conflict descriptions, e.g.:
+ - "sentiment-technical: Bearish sentiment conflicts with bullish technical (72)"
+ - "momentum-technical: Momentum (35) diverges from technical (68) by 33 points"
+ """
+```
+
+**Algorithm**:
+```
+Conflicts detected:
+
+1. Sentiment-Technical conflict:
+ - If sentiment="bearish" AND technical > 60: flag conflict
+ - If sentiment="bullish" AND technical < 40: flag conflict
+ - Message: "sentiment-technical: {sentiment} sentiment conflicts with {direction} technical ({score})"
+
+2. Momentum-Technical divergence:
+ - If abs(momentum - technical) > 30: flag conflict
+ - Message: "momentum-technical: Momentum ({momentum}) diverges from technical ({technical}) by {diff} points"
+
+3. Sentiment-Momentum conflict:
+ - If sentiment="bearish" AND momentum > 60: flag conflict
+ - If sentiment="bullish" AND momentum < 40: flag conflict
+ - Message: "sentiment-momentum: {sentiment} sentiment conflicts with momentum ({score})"
+
+4. Fundamental-Technical divergence (informational only):
+ - If abs(fundamental - technical) > 40: flag as "weak conflict"
+ - Message: "fundamental-technical: Fundamental ({fund}) diverges significantly from technical ({tech})"
+
+Return list of all detected conflicts
+```
+
+**Impact on Confidence**:
+- Each conflict reduces confidence by 15-25%:
+ - Sentiment-Technical: -20%
+ - Momentum-Technical: -15%
+ - Sentiment-Momentum: -20%
+ - Fundamental-Technical: -10% (weaker impact)
+- Applied in `direction_analyzer.calculate_confidence()` after base calculation
+
+**Rationale**: Conflicts indicate uncertainty and increase risk. Sentiment-technical conflicts are most serious as they represent narrative vs. price action divergence.
+
+
+
+## Data Models
+
+### Extended TradeSetup Model
+
+**New Fields**:
+```python
+class TradeSetup(Base):
+ __tablename__ = "trade_setups"
+
+ # Existing fields (unchanged)
+ id: Mapped[int] = mapped_column(primary_key=True)
+ ticker_id: Mapped[int] = mapped_column(ForeignKey("tickers.id", ondelete="CASCADE"))
+ direction: Mapped[str] = mapped_column(String(10), nullable=False)
+ entry_price: Mapped[float] = mapped_column(Float, nullable=False)
+ stop_loss: Mapped[float] = mapped_column(Float, nullable=False)
+ target: Mapped[float] = mapped_column(Float, nullable=False) # Primary target
+ rr_ratio: Mapped[float] = mapped_column(Float, nullable=False) # Primary R:R
+ composite_score: Mapped[float] = mapped_column(Float, nullable=False)
+ detected_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
+
+ # NEW: Recommendation fields
+ confidence_score: Mapped[float | None] = mapped_column(Float, nullable=True)
+ targets_json: Mapped[str | None] = mapped_column(Text, nullable=True)
+ conflict_flags_json: Mapped[str | None] = mapped_column(Text, nullable=True)
+ recommended_action: Mapped[str | None] = mapped_column(String(20), nullable=True)
+ reasoning: Mapped[str | None] = mapped_column(Text, nullable=True)
+ risk_level: Mapped[str | None] = mapped_column(String(10), nullable=True)
+```
+
+**Field Descriptions**:
+
+- `confidence_score`: Float 0-100, confidence in this direction
+- `targets_json`: JSON array of target objects (see schema below)
+- `conflict_flags_json`: JSON array of conflict strings
+- `recommended_action`: Enum-like string: "LONG_HIGH", "LONG_MODERATE", "SHORT_HIGH", "SHORT_MODERATE", "NEUTRAL"
+- `reasoning`: Human-readable explanation of recommendation
+- `risk_level`: "Low", "Medium", or "High" based on conflicts
+
+**Targets JSON Schema**:
+```json
+[
+ {
+ "price": 150.25,
+ "distance_from_entry": 5.25,
+ "distance_atr_multiple": 2.5,
+ "rr_ratio": 3.5,
+ "probability": 65.0,
+ "classification": "Conservative",
+ "sr_level_id": 42,
+ "sr_strength": 75
+ },
+ ...
+]
+```
+
+**Conflict Flags JSON Schema**:
+```json
+[
+ "sentiment-technical: Bearish sentiment conflicts with bullish technical (72)",
+ "momentum-technical: Momentum (35) diverges from technical (68) by 33 points"
+]
+```
+
+**Backward Compatibility**:
+- All new fields are nullable
+- Existing `target` and `rr_ratio` fields remain as primary target data
+- Old consumers can ignore new fields
+- New consumers use `targets_json` for full target list
+
+
+
+### SystemSetting Extensions
+
+**New Configuration Keys**:
+
+```python
+# Recommendation thresholds
+"recommendation_high_confidence_threshold": 70.0 # % for "High Confidence"
+"recommendation_moderate_confidence_threshold": 50.0 # % for "Moderate Confidence"
+"recommendation_confidence_diff_threshold": 20.0 # % difference for directional recommendation
+
+# Probability calculation weights
+"recommendation_signal_alignment_weight": 0.15 # 15%
+"recommendation_sr_strength_weight": 0.20 # 20%
+"recommendation_distance_penalty_factor": 0.10 # 10%
+
+# Conflict detection thresholds
+"recommendation_momentum_technical_divergence_threshold": 30.0 # points
+"recommendation_fundamental_technical_divergence_threshold": 40.0 # points
+```
+
+**Access Pattern**:
+```python
+async def get_recommendation_config(db: AsyncSession) -> dict:
+ """Load all recommendation configuration from SystemSetting."""
+ # Query all keys starting with "recommendation_"
+ # Return dict with defaults for missing keys
+```
+
+### No New Tables Required
+
+The design intentionally avoids new tables to minimize schema complexity:
+- TradeSetup extensions handle all recommendation data
+- JSON fields provide flexibility for evolving data structures
+- SystemSetting stores configuration
+- Existing relationships (Ticker → TradeSetup) remain unchanged
+
+
+
+## Algorithm Design
+
+### Confidence Scoring Formula
+
+**Detailed Implementation**:
+
+```python
+def calculate_confidence(
+ direction: str,
+ dimension_scores: dict[str, float],
+ sentiment_classification: str | None,
+ conflicts: list[str],
+) -> float:
+ """Calculate confidence score with conflict penalties."""
+
+ base = 50.0
+ technical = dimension_scores.get("technical", 50.0)
+ momentum = dimension_scores.get("momentum", 50.0)
+ fundamental = dimension_scores.get("fundamental", 50.0)
+
+ if direction == "long":
+ # Technical contribution
+ if technical > 70:
+ base += 25.0
+ elif technical > 60:
+ base += 15.0
+
+ # Momentum contribution
+ if momentum > 70:
+ base += 20.0
+ elif momentum > 60:
+ base += 15.0
+
+ # Sentiment contribution
+ if sentiment_classification == "bullish":
+ base += 15.0
+ elif sentiment_classification == "neutral":
+ base += 5.0
+
+ # Fundamental contribution
+ if fundamental > 60:
+ base += 10.0
+
+ elif direction == "short":
+ # Technical contribution
+ if technical < 30:
+ base += 25.0
+ elif technical < 40:
+ base += 15.0
+
+ # Momentum contribution
+ if momentum < 30:
+ base += 20.0
+ elif momentum < 40:
+ base += 15.0
+
+ # Sentiment contribution
+ if sentiment_classification == "bearish":
+ base += 15.0
+ elif sentiment_classification == "neutral":
+ base += 5.0
+
+ # Fundamental contribution
+ if fundamental < 40:
+ base += 10.0
+
+ # Apply conflict penalties
+ for conflict in conflicts:
+ if "sentiment-technical" in conflict:
+ base -= 20.0
+ elif "momentum-technical" in conflict:
+ base -= 15.0
+ elif "sentiment-momentum" in conflict:
+ base -= 20.0
+ elif "fundamental-technical" in conflict:
+ base -= 10.0
+
+ return max(0.0, min(100.0, base))
+```
+
+**Scoring Breakdown**:
+- Base: 50 points (neutral starting point)
+- Technical: up to 25 points (most important - reflects price action)
+- Momentum: up to 20 points (confirms trend strength)
+- Sentiment: up to 15 points (narrative support)
+- Fundamental: up to 10 points (value support)
+- Maximum possible: 120 points before conflicts
+- Conflicts: -10 to -20 points each
+
+
+
+### Probability Calculation Formula
+
+**Detailed Implementation**:
+
+```python
+def estimate_probability(
+ target: dict,
+ dimension_scores: dict[str, float],
+ sentiment_classification: str | None,
+ direction: str,
+ config: dict,
+) -> float:
+ """Estimate probability of reaching a price target."""
+
+ # 1. Base probability from classification (40% weight)
+ classification = target["classification"]
+ if classification == "Conservative":
+ base_prob = 70.0
+ elif classification == "Moderate":
+ base_prob = 55.0
+ else: # Aggressive
+ base_prob = 40.0
+
+ # 2. S/R strength adjustment (30% weight)
+ strength = target["sr_strength"]
+ if strength >= 80:
+ strength_adj = 15.0
+ elif strength >= 60:
+ strength_adj = 10.0
+ elif strength >= 40:
+ strength_adj = 5.0
+ else:
+ strength_adj = -10.0
+
+ # 3. Signal alignment adjustment (20% weight)
+ technical = dimension_scores.get("technical", 50.0)
+ momentum = dimension_scores.get("momentum", 50.0)
+
+ alignment_adj = 0.0
+ if direction == "long":
+ if technical > 60 and (sentiment_classification == "bullish" or momentum > 60):
+ alignment_adj = 15.0
+ elif technical < 40 or (sentiment_classification == "bearish" and momentum < 40):
+ alignment_adj = -15.0
+ elif direction == "short":
+ if technical < 40 and (sentiment_classification == "bearish" or momentum < 40):
+ alignment_adj = 15.0
+ elif technical > 60 or (sentiment_classification == "bullish" and momentum > 60):
+ alignment_adj = -15.0
+
+ # 4. Volatility adjustment (10% weight)
+ atr_multiple = target["distance_atr_multiple"]
+ volatility_adj = 0.0
+ if atr_multiple > 5:
+ volatility_adj = 5.0 # High volatility favors distant targets
+ elif atr_multiple < 2:
+ volatility_adj = 5.0 # Low volatility favors near targets
+
+ # Combine all factors
+ probability = base_prob + strength_adj + alignment_adj + volatility_adj
+
+ # Clamp to [10, 90] to reflect uncertainty
+ return max(10.0, min(90.0, probability))
+```
+
+**Probability Ranges by Classification**:
+- Conservative: 60-90% (typically 70-85%)
+- Moderate: 40-70% (typically 50-65%)
+- Aggressive: 10-50% (typically 30-45%)
+
+
+
+### Signal Alignment Logic
+
+**Implementation**:
+
+```python
+def check_signal_alignment(
+ direction: str,
+ dimension_scores: dict[str, float],
+ sentiment_classification: str | None,
+) -> tuple[bool, str]:
+ """Check if signals align with the trade direction.
+
+ Returns:
+ (is_aligned, description)
+ """
+ technical = dimension_scores.get("technical", 50.0)
+ momentum = dimension_scores.get("momentum", 50.0)
+
+ if direction == "long":
+ tech_bullish = technical > 60
+ momentum_bullish = momentum > 60
+ sentiment_bullish = sentiment_classification == "bullish"
+
+ # Need at least 2 of 3 signals aligned
+ aligned_count = sum([tech_bullish, momentum_bullish, sentiment_bullish])
+
+ if aligned_count >= 2:
+ return True, f"Signals aligned for LONG: technical={technical:.0f}, momentum={momentum:.0f}, sentiment={sentiment_classification}"
+ else:
+ return False, f"Mixed signals for LONG: technical={technical:.0f}, momentum={momentum:.0f}, sentiment={sentiment_classification}"
+
+ elif direction == "short":
+ tech_bearish = technical < 40
+ momentum_bearish = momentum < 40
+ sentiment_bearish = sentiment_classification == "bearish"
+
+ # Need at least 2 of 3 signals aligned
+ aligned_count = sum([tech_bearish, momentum_bearish, sentiment_bearish])
+
+ if aligned_count >= 2:
+ return True, f"Signals aligned for SHORT: technical={technical:.0f}, momentum={momentum:.0f}, sentiment={sentiment_classification}"
+ else:
+ return False, f"Mixed signals for SHORT: technical={technical:.0f}, momentum={momentum:.0f}, sentiment={sentiment_classification}"
+
+ return False, "Unknown direction"
+```
+
+**Alignment Criteria**:
+- LONG: At least 2 of [technical > 60, momentum > 60, sentiment bullish]
+- SHORT: At least 2 of [technical < 40, momentum < 40, sentiment bearish]
+- Fundamental score is informational but not required for alignment
+
+
+
+## API Design
+
+### Enhanced Trade Setup Endpoints
+
+**GET /api/v1/trades**
+
+Returns all trade setups with recommendation data.
+
+**Query Parameters**:
+- `direction`: Optional filter ("long" or "short")
+- `min_confidence`: Optional minimum confidence score (0-100)
+- `recommended_action`: Optional filter ("LONG_HIGH", "LONG_MODERATE", "SHORT_HIGH", "SHORT_MODERATE", "NEUTRAL")
+
+**Response Schema**:
+```json
+{
+ "status": "success",
+ "data": [
+ {
+ "id": 1,
+ "symbol": "AAPL",
+ "direction": "long",
+ "entry_price": 145.00,
+ "stop_loss": 142.50,
+ "target": 150.00,
+ "rr_ratio": 2.0,
+ "composite_score": 75.5,
+ "detected_at": "2024-01-15T10:30:00Z",
+
+ "confidence_score": 72.5,
+ "recommended_action": "LONG_HIGH",
+ "reasoning": "Strong technical (75) and bullish sentiment align with upward momentum (68). No major conflicts detected.",
+ "risk_level": "Low",
+
+ "targets": [
+ {
+ "price": 147.50,
+ "distance_from_entry": 2.50,
+ "distance_atr_multiple": 1.5,
+ "rr_ratio": 1.0,
+ "probability": 75.0,
+ "classification": "Conservative",
+ "sr_level_id": 42,
+ "sr_strength": 80
+ },
+ {
+ "price": 150.00,
+ "distance_from_entry": 5.00,
+ "distance_atr_multiple": 3.0,
+ "rr_ratio": 2.0,
+ "probability": 60.0,
+ "classification": "Moderate",
+ "sr_level_id": 43,
+ "sr_strength": 70
+ },
+ {
+ "price": 155.00,
+ "distance_from_entry": 10.00,
+ "distance_atr_multiple": 6.0,
+ "rr_ratio": 4.0,
+ "probability": 35.0,
+ "classification": "Aggressive",
+ "sr_level_id": 44,
+ "sr_strength": 60
+ }
+ ],
+
+ "conflict_flags": []
+ }
+ ]
+}
+```
+
+**GET /api/v1/trades/{symbol}**
+
+Returns trade setups for a specific ticker (both LONG and SHORT if available).
+
+**Response**: Same schema as above, filtered by symbol.
+
+
+
+### Admin Configuration Endpoints
+
+**GET /api/v1/admin/settings/recommendations**
+
+Get current recommendation configuration.
+
+**Response**:
+```json
+{
+ "status": "success",
+ "data": {
+ "high_confidence_threshold": 70.0,
+ "moderate_confidence_threshold": 50.0,
+ "confidence_diff_threshold": 20.0,
+ "signal_alignment_weight": 0.15,
+ "sr_strength_weight": 0.20,
+ "distance_penalty_factor": 0.10,
+ "momentum_technical_divergence_threshold": 30.0,
+ "fundamental_technical_divergence_threshold": 40.0
+ }
+}
+```
+
+**PUT /api/v1/admin/settings/recommendations**
+
+Update recommendation configuration.
+
+**Request Body**:
+```json
+{
+ "high_confidence_threshold": 75.0,
+ "signal_alignment_weight": 0.20
+}
+```
+
+**Response**: Updated configuration object.
+
+**Validation**:
+- All thresholds must be 0-100
+- All weights must be 0-1
+- Returns 400 error for invalid values
+
+
+
+## Frontend Components
+
+### Ticker Detail Page Enhancement
+
+**Location**: `frontend/src/components/ticker/RecommendationPanel.tsx`
+
+**Component Structure**:
+```tsx
+interface RecommendationPanelProps {
+ symbol: string;
+ longSetup?: TradeSetup;
+ shortSetup?: TradeSetup;
+}
+
+export function RecommendationPanel({ symbol, longSetup, shortSetup }: RecommendationPanelProps) {
+ // Display recommendation summary at top
+ // Show LONG and SHORT setups side-by-side
+ // Highlight recommended direction
+ // Display targets table for each direction
+ // Show conflict warnings if present
+}
+```
+
+**Visual Design**:
+- Recommendation summary card at top with large action text and confidence badge
+- Two-column layout: LONG setup on left, SHORT setup on right
+- Recommended direction has green border and subtle glow
+- Non-recommended direction has muted opacity
+- Risk level badge: green (Low), yellow (Medium), red (High)
+- Targets table with sortable columns
+- Conflict warnings in amber alert box
+
+**Data Flow**:
+```tsx
+// In TickerDetailPage.tsx
+const { data: tradeSetups } = useTradeSetups(symbol);
+
+const longSetup = tradeSetups?.find(s => s.direction === 'long');
+const shortSetup = tradeSetups?.find(s => s.direction === 'short');
+
+
{(error as Error)?.message || 'Failed to load pipeline readiness'}
; + + const rows = data ?? []; + + return ( +Shows why tickers may be missing in scanner/rankings and what is incomplete.
+No tickers available.
+ ) : ( +| Symbol | +OHLCV | +Dims | +S/R | +Scanner | +Missing Reasons | +Action | +
|---|---|---|---|---|---|---|
| {row.symbol} | +
+ {row.ohlcv_bars} bars
+ {row.ohlcv_last_date ? formatDateTime(row.ohlcv_last_date) : '—'}
+ |
+
+
+ {scoreBadge(row.dimensions.technical)}
+ {scoreBadge(row.dimensions.sr_quality)}
+ {scoreBadge(row.dimensions.sentiment)}
+ {scoreBadge(row.dimensions.fundamental)}
+ {scoreBadge(row.dimensions.momentum)}
+
+ T SR S F M
+ |
+ {row.sr_level_count} | +
+
+ {row.ready_for_scanner ? 'Ready' : 'Blocked'}
+
+ setups: {row.trade_setup_count}
+ |
+ + {row.missing_reasons.length ? row.missing_reasons.join(', ') : none} + | ++ + | +
{(error as Error)?.message || 'Failed to load recommendation settings'}
; + + return ( ++ Auto-discover tickers from a predefined universe and keep your registry updated. +
+ + {isError && ( ++ {(error as Error)?.message || 'Failed to load ticker universe setting'} +
+ )} + +No trade setups match the current filters.
; @@ -84,6 +102,17 @@ export function TradeTable({ trades, sortColumn, sortDirection, onSort }: TradeT {trade.symbol} +No target probabilities available.
; + } + + return ( +| Classification | +Price | +Distance | +R:R | +Probability | +
|---|---|---|---|---|
| {target.classification} | +{formatPrice(target.price)} | +{formatPercent((target.distance_from_entry / setup.entry_price) * 100)} | +{target.rr_ratio.toFixed(2)} | +{target.probability.toFixed(1)}% | +
Alternative setup (ticker bias currently favors the opposite direction).
+ )} + +Recommended Action is the ticker-level bias. The preferred setup is shown first; the opposite side is available under Alternative scenario.
+ + {summary?.reasoning && ( +{summary.reasoning}
+ )} + + {preferredDirection !== 'neutral' && preferredSetup ? ( +Recommended Action Glossary (Ticker-Level Bias)
++ {RECOMMENDATION_ACTION_LABELS[item.action]}:{' '} + {item.description} +
+ ))} +Ticker Detail