trading/docs/plans/2026-02-22-trading-bot-implementation.md
Viktor Barzin 9d9f291889
Add trading bot implementation plan — 18 tasks across 7 phases
Covers: foundation, docker infra, models, schemas, broker abstraction,
news pipeline, sentiment analysis, strategies, signal generation,
trade execution, learning engine, backtesting, API gateway with
passkey auth, React dashboard, containerization, and integration tests.

[ci skip]
2026-02-22 15:03:58 +00:00

1149 lines
40 KiB
Markdown

# Trading Bot Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Build an automated stock trading bot that combines news sentiment analysis with technical strategies, learns from outcomes, and provides a real-time dashboard.
**Architecture:** Event-driven Python microservices communicating via Redis Streams, with PostgreSQL+TimescaleDB for persistence, Alpaca for brokerage, and a React/TypeScript dashboard. See `docs/plans/2026-02-22-trading-bot-design.md` for full design.
**Tech Stack:** Python 3.12, FastAPI, SQLAlchemy (async), Redis Streams, PostgreSQL+TimescaleDB, React 18, TypeScript, Tailwind CSS, alpaca-py, transformers (FinBERT), Ollama, OpenTelemetry, py-webauthn
---
## Phase 1: Foundation
### Task 1: Python Monorepo Setup
**Files:**
- Create: `pyproject.toml`
- Create: `shared/__init__.py`
- Create: `shared/config.py`
- Create: `shared/redis_streams.py`
- Create: `shared/telemetry.py`
- Create: `tests/__init__.py`
**Step 1: Create pyproject.toml**
Single pyproject.toml at the repo root. Use `[project.optional-dependencies]` groups per service so each service only installs what it needs. Core deps shared by all: `sqlalchemy[asyncio]`, `asyncpg`, `redis`, `pydantic`, `pydantic-settings`, `opentelemetry-sdk`, `opentelemetry-exporter-prometheus`, `opentelemetry-api`.
```toml
[project]
name = "trading-bot"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"sqlalchemy[asyncio]>=2.0",
"asyncpg>=0.29",
"redis>=5.0",
"pydantic>=2.0",
"pydantic-settings>=2.0",
"opentelemetry-sdk>=1.20",
"opentelemetry-exporter-prometheus>=0.45b",
"opentelemetry-api>=1.20",
"alembic>=1.13",
]
[project.optional-dependencies]
api = ["fastapi>=0.110", "uvicorn[standard]>=0.27", "websockets>=12.0", "py-webauthn>=2.0", "pyjwt[crypto]>=2.8"]
news = ["feedparser>=6.0", "praw>=7.7", "httpx>=0.27"]
sentiment = ["transformers>=4.38", "torch>=2.2", "ollama>=0.1"]
trading = ["alpaca-py>=0.21"]
backtester = ["numpy>=1.26", "pandas>=2.2"]
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "pytest-cov>=4.1", "ruff>=0.3", "mypy>=1.8"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
[tool.ruff]
line-length = 120
target-version = "py312"
```
**Step 2: Create shared config module**
`shared/config.py` — Pydantic Settings class for all shared config (database URL, Redis URL, etc). Each service extends this with its own settings.
```python
from pydantic_settings import BaseSettings
class BaseConfig(BaseSettings):
database_url: str = "postgresql+asyncpg://trading:trading@localhost:5432/trading"
redis_url: str = "redis://localhost:6379/0"
log_level: str = "INFO"
otel_service_name: str = "trading-bot"
otel_metrics_port: int = 9090
model_config = {"env_prefix": "TRADING_"}
```
**Step 3: Create Redis Streams helper**
`shared/redis_streams.py` — Thin wrapper around redis-py Streams for publishing and consuming. Consumer groups, auto-ack, deserialization.
```python
import json
import logging
from typing import AsyncIterator
from redis.asyncio import Redis
logger = logging.getLogger(__name__)
class StreamPublisher:
def __init__(self, redis: Redis, stream: str):
self.redis = redis
self.stream = stream
async def publish(self, data: dict) -> str:
msg_id = await self.redis.xadd(self.stream, {"data": json.dumps(data)})
return msg_id
class StreamConsumer:
def __init__(self, redis: Redis, stream: str, group: str, consumer: str):
self.redis = redis
self.stream = stream
self.group = group
self.consumer = consumer
async def ensure_group(self) -> None:
try:
await self.redis.xgroup_create(self.stream, self.group, id="0", mkstream=True)
except Exception:
pass # group already exists
async def consume(self, batch_size: int = 10, block_ms: int = 5000) -> AsyncIterator[tuple[str, dict]]:
await self.ensure_group()
while True:
messages = await self.redis.xreadgroup(
self.group, self.consumer, {self.stream: ">"}, count=batch_size, block=block_ms
)
for _stream, entries in messages:
for msg_id, fields in entries:
data = json.loads(fields[b"data"])
yield msg_id, data
await self.redis.xack(self.stream, self.group, msg_id)
```
**Step 4: Create OpenTelemetry helper**
`shared/telemetry.py` — Sets up meter provider with Prometheus exporter, returns a meter. Each service calls `setup_telemetry("service-name")` at startup and starts a `/metrics` HTTP endpoint.
```python
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from prometheus_client import start_http_server
def setup_telemetry(service_name: str, metrics_port: int = 9090) -> metrics.Meter:
reader = PrometheusMetricReader()
provider = MeterProvider(metric_readers=[reader])
metrics.set_meter_provider(provider)
start_http_server(metrics_port)
return metrics.get_meter(service_name)
```
**Step 5: Write tests for Redis Streams helper**
```python
# tests/test_redis_streams.py
import pytest
from unittest.mock import AsyncMock, MagicMock
from shared.redis_streams import StreamPublisher, StreamConsumer
@pytest.mark.asyncio
async def test_publisher_publishes_json():
redis = AsyncMock()
redis.xadd = AsyncMock(return_value=b"1-0")
pub = StreamPublisher(redis, "test:stream")
msg_id = await pub.publish({"ticker": "AAPL", "score": 0.8})
redis.xadd.assert_called_once()
assert msg_id == b"1-0"
```
Run: `python -m pytest tests/test_redis_streams.py -v`
**Step 6: Commit**
```bash
git add pyproject.toml shared/ tests/
git commit -m "feat: project foundation — monorepo setup, shared config, redis streams, telemetry"
```
---
### Task 2: Docker Compose Infrastructure
**Files:**
- Create: `docker-compose.yml`
- Create: `.env.example`
- Create: `.gitignore`
**Step 1: Create docker-compose.yml with infrastructure services**
Start with just the infrastructure containers (postgres+timescaledb, redis, ollama). Application services added later.
```yaml
services:
postgres:
image: timescale/timescaledb:latest-pg16
environment:
POSTGRES_USER: trading
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-trading}
POSTGRES_DB: trading
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U trading"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
ollama:
image: ollama/ollama:latest
ports:
- "11434:11434"
volumes:
- ollama_models:/root/.ollama
volumes:
pgdata:
redisdata:
ollama_models:
```
**Step 2: Create .env.example and .gitignore**
`.env.example`: document all env vars with safe defaults.
`.gitignore`: Python defaults + `.env`, `__pycache__`, `.venv`, node_modules, etc.
**Step 3: Boot infrastructure and verify**
```bash
docker compose up -d postgres redis
docker compose ps # verify healthy
```
**Step 4: Commit**
```bash
git add docker-compose.yml .env.example .gitignore
git commit -m "feat: docker compose infrastructure — postgres+timescaledb, redis, ollama"
```
---
### Task 3: Database Models & Alembic Migrations
**Files:**
- Create: `shared/models/__init__.py`
- Create: `shared/models/base.py`
- Create: `shared/models/trading.py`
- Create: `shared/models/news.py`
- Create: `shared/models/learning.py`
- Create: `shared/models/auth.py`
- Create: `shared/models/timeseries.py`
- Create: `shared/db.py`
- Create: `alembic.ini`
- Create: `alembic/env.py`
- Create: `tests/test_models.py`
**Step 1: Create base model and DB session factory**
`shared/models/base.py` — Declarative base with common columns (id, created_at, updated_at).
`shared/db.py` — Async engine + sessionmaker factory.
```python
# shared/models/base.py
import uuid
from datetime import datetime, timezone
from sqlalchemy import DateTime, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID
class Base(DeclarativeBase):
pass
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
```
```python
# shared/db.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from shared.config import BaseConfig
def create_db(config: BaseConfig) -> tuple:
engine = create_async_engine(config.database_url, echo=config.log_level == "DEBUG")
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
return engine, session_factory
```
**Step 2: Create trading models**
`shared/models/trading.py``Trade`, `Position`, `Signal`, `Strategy`, `StrategyWeightHistory` per design doc.
Key columns per design doc:
- `trades`: ticker, side (enum: BUY/SELL), qty, price, timestamp, strategy_id (FK), signal_id (FK), status (enum: PENDING/FILLED/CANCELLED/REJECTED), pnl
- `positions`: ticker (unique), qty, avg_entry, unrealized_pnl, stop_loss, take_profit
- `signals`: ticker, direction (enum: LONG/SHORT/NEUTRAL), strength (float 0-1), strategy_sources (JSON), sentiment_score, acted_on (bool)
- `strategies`: name (unique), description, current_weight, active (bool)
- `strategy_weights_history`: strategy_id (FK), old_weight, new_weight, timestamp, reason
**Step 3: Create news models**
`shared/models/news.py``Article`, `ArticleSentiment` per design doc.
**Step 4: Create learning models**
`shared/models/learning.py``TradeOutcome`, `LearningAdjustment` per design doc.
**Step 5: Create auth models**
`shared/models/auth.py``User`, `UserCredential` per design doc.
**Step 6: Create timeseries models**
`shared/models/timeseries.py``MarketData`, `PortfolioSnapshot`, `StrategyMetric`. These will be TimescaleDB hypertables (created via migration with `SELECT create_hypertable(...)`).
**Step 7: Set up Alembic**
```bash
python -m alembic init alembic
```
Edit `alembic/env.py` to import all models and use async engine. Edit `alembic.ini` to read `sqlalchemy.url` from env.
**Step 8: Generate and apply initial migration**
```bash
python -m alembic revision --autogenerate -m "initial schema"
```
Manually edit migration to add TimescaleDB hypertable creation after table creates:
```python
op.execute("SELECT create_hypertable('market_data', 'timestamp')")
op.execute("SELECT create_hypertable('portfolio_snapshots', 'timestamp')")
op.execute("SELECT create_hypertable('strategy_metrics', 'timestamp')")
```
Apply:
```bash
docker compose up -d postgres
python -m alembic upgrade head
```
**Step 9: Write model tests**
Test that models can be instantiated, relationships work, enums are correct.
**Step 10: Commit**
```bash
git add shared/models/ shared/db.py alembic/ alembic.ini tests/test_models.py
git commit -m "feat: database models and alembic migrations — all tables per design"
```
---
### Task 4: Pydantic Schemas
**Files:**
- Create: `shared/schemas/__init__.py`
- Create: `shared/schemas/trading.py`
- Create: `shared/schemas/news.py`
- Create: `shared/schemas/learning.py`
- Create: `shared/schemas/auth.py`
- Create: `tests/test_schemas.py`
**Step 1: Create schemas matching Redis Stream message formats**
These are the Pydantic v2 models used for (de)serialization across services. Each Redis Stream message is one of these schemas serialized to JSON.
Key schemas:
- `RawArticle` (published to `news:raw`): source, url, title, content, published_at, fetched_at
- `ScoredArticle` (published to `news:scored`): article fields + ticker, sentiment_score, confidence, model_used, entities
- `TradeSignal` (published to `signals:generated`): ticker, direction, strength, strategy_sources, sentiment_context, timestamp
- `TradeExecution` (published to `trades:executed`): trade_id, ticker, side, qty, price, status, signal_id, timestamp
Also create request/response schemas for the API Gateway endpoints.
**Step 2: Write schema validation tests**
Test serialization round-trips, validation constraints (score range, required fields).
**Step 3: Commit**
```bash
git add shared/schemas/ tests/test_schemas.py
git commit -m "feat: pydantic schemas for all service message types"
```
---
## Phase 2: Brokerage & Market Data
### Task 5: Brokerage Abstraction Layer
**Files:**
- Create: `shared/broker/__init__.py`
- Create: `shared/broker/base.py`
- Create: `shared/broker/alpaca_broker.py`
- Create: `tests/test_broker.py`
**Step 1: Define abstract broker interface**
`shared/broker/base.py` — ABC with methods: `submit_order`, `cancel_order`, `get_positions`, `get_account`, `get_order_status`, `stream_market_data`. This is the seam that lets us swap Alpaca for another brokerage later.
```python
from abc import ABC, abstractmethod
from shared.schemas.trading import OrderRequest, OrderResult, PositionInfo, AccountInfo
class BaseBroker(ABC):
@abstractmethod
async def submit_order(self, order: OrderRequest) -> OrderResult: ...
@abstractmethod
async def cancel_order(self, order_id: str) -> bool: ...
@abstractmethod
async def get_positions(self) -> list[PositionInfo]: ...
@abstractmethod
async def get_account(self) -> AccountInfo: ...
@abstractmethod
async def get_order_status(self, order_id: str) -> OrderResult: ...
```
**Step 2: Implement Alpaca broker**
`shared/broker/alpaca_broker.py` — Wraps `alpaca-py` SDK. Uses `TradingClient` for orders/positions/account, `StockDataStream` for real-time bars. Paper vs live controlled by config (API base URL).
**Step 3: Write tests with mocked Alpaca client**
Test order submission, position retrieval, error handling (rejected orders, network failures).
**Step 4: Commit**
```bash
git add shared/broker/ tests/test_broker.py
git commit -m "feat: brokerage abstraction layer with Alpaca implementation"
```
---
## Phase 3: News Pipeline
### Task 6: News Fetcher Service
**Files:**
- Create: `services/news-fetcher/__init__.py`
- Create: `services/news-fetcher/main.py`
- Create: `services/news-fetcher/sources/__init__.py`
- Create: `services/news-fetcher/sources/rss.py`
- Create: `services/news-fetcher/sources/reddit.py`
- Create: `services/news-fetcher/config.py`
- Create: `tests/services/test_news_fetcher.py`
**Step 1: Create RSS source**
Uses `feedparser` to poll configurable list of RSS feed URLs. Deduplicates by content_hash (SHA256 of URL+title). Converts feed entries to `RawArticle` schema. Configurable poll interval.
Default feeds: Yahoo Finance, Reuters business, MarketWatch, SEC EDGAR RSS.
**Step 2: Create Reddit source**
Uses `praw` (async via `asyncpraw`) to poll r/wallstreetbets, r/stocks, r/investing. Fetches hot/new posts above score threshold. Converts to `RawArticle` schema.
**Step 3: Create main service loop**
`main.py` — Async service that:
1. Loads config (feed URLs, poll intervals, Reddit credentials)
2. Connects to Redis
3. Sets up telemetry (articles_fetched counter, fetch_errors counter, fetch_latency histogram)
4. Runs source pollers on configurable schedules via `asyncio.TaskGroup`
5. Publishes each `RawArticle` to `news:raw` stream
**Step 4: Write tests**
Test RSS parsing with fixture XML, Reddit post conversion, deduplication logic, stream publishing.
**Step 5: Commit**
```bash
git add services/news-fetcher/ tests/services/test_news_fetcher.py
git commit -m "feat: news fetcher service — RSS and Reddit sources"
```
---
### Task 7: Sentiment Analyzer Service
**Files:**
- Create: `services/sentiment-analyzer/__init__.py`
- Create: `services/sentiment-analyzer/main.py`
- Create: `services/sentiment-analyzer/analyzers/__init__.py`
- Create: `services/sentiment-analyzer/analyzers/finbert.py`
- Create: `services/sentiment-analyzer/analyzers/ollama_analyzer.py`
- Create: `services/sentiment-analyzer/ticker_extractor.py`
- Create: `services/sentiment-analyzer/config.py`
- Create: `tests/services/test_sentiment_analyzer.py`
**Step 1: Create FinBERT analyzer**
Loads `ProsusAI/finbert` via HuggingFace transformers. Input: article title + first 512 tokens of content. Output: sentiment score (-1 to +1), confidence (0 to 1). Runs on CPU by default (GPU if available via torch.cuda).
**Step 2: Create Ollama analyzer**
Fallback for articles where FinBERT confidence < configurable threshold (default 0.6). Uses `ollama` Python client to query local Mistral/Llama 3. Structured prompt asking for JSON output with sentiment score, reasoning, and entity extraction.
**Step 3: Create ticker extractor**
Regex + lookup against a known ticker list (can be loaded from Alpaca's asset list). Extracts mentioned stock tickers from article text. Handles common patterns: $AAPL, "Apple Inc", NASDAQ:AAPL.
**Step 4: Create main service loop**
Consumes `news:raw`, routes through FinBERT (if low confidence) Ollama, extracts tickers, publishes `ScoredArticle` to `news:scored`.
Telemetry: articles_scored counter, finbert_vs_ollama_ratio, inference_latency histogram.
**Step 5: Write tests**
Test FinBERT scoring (mock transformers pipeline), Ollama fallback routing, ticker extraction regex, end-to-end message flow.
**Step 6: Commit**
```bash
git add services/sentiment-analyzer/ tests/services/test_sentiment_analyzer.py
git commit -m "feat: sentiment analyzer — FinBERT + Ollama tiered analysis"
```
---
## Phase 4: Trading Core
### Task 8: Strategy Implementations
**Files:**
- Create: `shared/strategies/__init__.py`
- Create: `shared/strategies/base.py`
- Create: `shared/strategies/momentum.py`
- Create: `shared/strategies/mean_reversion.py`
- Create: `shared/strategies/news_driven.py`
- Create: `tests/test_strategies.py`
**Step 1: Define strategy interface**
```python
# shared/strategies/base.py
from abc import ABC, abstractmethod
from shared.schemas.trading import TradeSignal, MarketSnapshot, SentimentContext
class BaseStrategy(ABC):
name: str
@abstractmethod
async def evaluate(
self, ticker: str, market: MarketSnapshot, sentiment: SentimentContext | None = None
) -> TradeSignal | None:
"""Return a signal if this strategy has an opinion, None otherwise."""
...
```
`MarketSnapshot`: current price, OHLCV bars (recent history), volume profile, moving averages.
`SentimentContext`: recent sentiment scores for this ticker, article count, average confidence.
**Step 2: Implement momentum strategy**
Buy when price crosses above N-period SMA with increasing volume. Sell when crosses below. Signal strength proportional to distance from SMA.
**Step 3: Implement mean reversion strategy**
Buy when RSI < 30 (oversold), sell when RSI > 70 (overbought). Signal strength proportional to RSI extremity.
**Step 4: Implement news-driven strategy**
Buy on strong positive sentiment (score > 0.7, confidence > 0.6), sell on strong negative. Signal strength = sentiment_score * confidence. Decay factor for stale news (> 4 hours old).
**Step 5: Write tests for each strategy**
Test with known market data fixtures. Verify signal direction, strength ranges, edge cases (no data, flat market).
**Step 6: Commit**
```bash
git add shared/strategies/ tests/test_strategies.py
git commit -m "feat: trading strategies — momentum, mean reversion, news-driven"
```
---
### Task 9: Signal Generator Service
**Files:**
- Create: `services/signal-generator/__init__.py`
- Create: `services/signal-generator/main.py`
- Create: `services/signal-generator/ensemble.py`
- Create: `services/signal-generator/market_data.py`
- Create: `services/signal-generator/config.py`
- Create: `tests/services/test_signal_generator.py`
**Step 1: Create market data consumer**
Connects to Alpaca WebSocket (`StockDataStream`) for real-time bars/quotes. Maintains in-memory rolling window of recent OHLCV bars per subscribed ticker. Builds `MarketSnapshot` objects.
**Step 2: Create weighted ensemble**
Loads strategy weights from DB (or Redis cache). Runs all active strategies, combines signals:
- For each ticker with signals: weighted average of direction * strength * weight
- Apply threshold: only emit signal if combined strength > configurable min (default 0.3)
- Tag output signal with contributing strategy sources and their individual strengths
```python
async def combine_signals(
signals: list[tuple[BaseStrategy, TradeSignal]],
weights: dict[str, float],
) -> TradeSignal | None:
if not signals:
return None
weighted_sum = sum(
s.strength * (1 if s.direction == "LONG" else -1) * weights.get(strategy.name, 0.1)
for strategy, s in signals
)
total_weight = sum(weights.get(strategy.name, 0.1) for strategy, _ in signals)
combined_strength = abs(weighted_sum / total_weight) if total_weight > 0 else 0
# ... threshold check, build TradeSignal ...
```
**Step 3: Create main service loop**
Consumes `news:scored` (builds sentiment context per ticker). Subscribes to Alpaca WebSocket for tickers mentioned in recent news + a watchlist. On each new bar or sentiment update, runs ensemble for affected tickers. Publishes qualifying signals to `signals:generated`.
**Step 4: Write tests**
Test ensemble weighting math, threshold filtering, sentiment context aggregation, market snapshot building.
**Step 5: Commit**
```bash
git add services/signal-generator/ tests/services/test_signal_generator.py
git commit -m "feat: signal generator — weighted ensemble with market data"
```
---
### Task 10: Trade Executor Service
**Files:**
- Create: `services/trade-executor/__init__.py`
- Create: `services/trade-executor/main.py`
- Create: `services/trade-executor/risk_manager.py`
- Create: `services/trade-executor/config.py`
- Create: `tests/services/test_trade_executor.py`
**Step 1: Create risk manager**
Pre-trade risk checks:
- Position sizing: Kelly criterion or fixed fractional (configurable), max % of portfolio per position (default 5%)
- Max total exposure: sum of all position values < configurable % of portfolio (default 80%)
- Max positions: configurable cap (default 20)
- Stop-loss: auto-set at configurable % below entry (default 3%)
- Cooldown: no re-entry into same ticker within N minutes of exit (default 30)
- Market hours check: only trade during market hours (9:30 AM - 4:00 PM ET)
**Step 2: Create main service loop**
Consumes `signals:generated`. For each signal:
1. Run risk checks reject if any fail
2. Calculate position size
3. Submit order via broker abstraction
4. Record trade in PostgreSQL (status: PENDING)
5. Poll/await fill confirmation
6. Update trade record (status: FILLED, actual price)
7. Update positions table
8. Publish `TradeExecution` to `trades:executed`
Telemetry: trades_executed counter, order_fill_latency histogram, rejection_rate counter (by reason).
**Step 3: Write tests**
Test risk checks (position sizing math, exposure limits, cooldown), order flow with mocked broker, DB recording.
**Step 4: Commit**
```bash
git add services/trade-executor/ tests/services/test_trade_executor.py
git commit -m "feat: trade executor — risk management and order execution"
```
---
## Phase 5: Learning & Backtesting
### Task 11: Learning Engine Service
**Files:**
- Create: `services/learning-engine/__init__.py`
- Create: `services/learning-engine/main.py`
- Create: `services/learning-engine/evaluator.py`
- Create: `services/learning-engine/weight_adjuster.py`
- Create: `services/learning-engine/config.py`
- Create: `tests/services/test_learning_engine.py`
**Step 1: Create trade evaluator**
When a position is closed (detected via `trades:executed` with side opposite to open position):
- Compute realized P&L, ROI %, hold duration
- Compute risk-adjusted return (per-trade Sharpe approximation)
- Store `TradeOutcome` in DB
- Attribute credit/blame to contributing strategies proportionally to signal strength
**Step 2: Create weight adjuster**
Multi-armed bandit style (per design doc):
```python
new_weight = (1 - lr) * old_weight + lr * reward_signal
```
Guardrails (all configurable):
- Minimum 20 trades per strategy before any adjustment
- Max 10% weight shift per cycle
- Weight floor of 0.05
- Normalize all weights to sum to 1.0
- Exponential recency decay (recent trades weighted more)
Store every adjustment in `learning_adjustments` table. Update strategy weights in `strategies` table and Redis cache.
**Step 3: Create main service loop**
Consumes `trades:executed`. On position close events, runs evaluator weight adjuster. Periodically (configurable, default 1 hour) computes portfolio-level metrics and stores `PortfolioSnapshot` and `StrategyMetric` in TimescaleDB.
**Step 4: Write tests**
Test P&L calculation, credit attribution math, weight adjustment with guardrails (test each guardrail independently), normalization.
**Step 5: Commit**
```bash
git add services/learning-engine/ tests/services/test_learning_engine.py
git commit -m "feat: learning engine — multi-armed bandit strategy weight adjustment"
```
---
### Task 12: Backtesting Engine
**Files:**
- Create: `backtester/__init__.py`
- Create: `backtester/engine.py`
- Create: `backtester/simulated_broker.py`
- Create: `backtester/data_loader.py`
- Create: `backtester/metrics.py`
- Create: `backtester/config.py`
- Create: `tests/test_backtester.py`
**Step 1: Create simulated broker**
Implements `BaseBroker` interface. Fills orders instantly at current bar's close price (or configurable slippage model). Tracks simulated positions and cash balance. Supports configurable commission model.
**Step 2: Create data loader**
Loads historical OHLCV bars from TimescaleDB `market_data` table. Loads historical sentiment from `article_sentiments` table. Aligns by timestamp. Returns an async iterator of `(timestamp, MarketSnapshot, SentimentContext | None)` tuples.
**Step 3: Create backtest engine**
Replays data loader output through the same strategy ensemble risk manager simulated broker pipeline. Records all simulated trades. Uses the same code as live system (shared strategies, shared ensemble, shared risk manager).
**Step 4: Create metrics calculator**
From the simulated trade log, compute:
- Equity curve (portfolio value over time)
- Total return, annualized return
- Sharpe ratio, Sortino ratio
- Max drawdown (% and duration)
- Win rate, average win/loss ratio
- Per-strategy attribution (which strategies contributed most)
- Trade count, average hold duration
**Step 5: Write tests**
Test simulated broker fills, equity curve calculation, metrics math (known inputs known outputs), data loader query.
**Step 6: Commit**
```bash
git add backtester/ tests/test_backtester.py
git commit -m "feat: backtesting engine — historical replay with shared strategies"
```
---
## Phase 6: API & Dashboard
### Task 13: API Gateway — Auth
**Files:**
- Create: `services/api-gateway/__init__.py`
- Create: `services/api-gateway/main.py`
- Create: `services/api-gateway/auth/__init__.py`
- Create: `services/api-gateway/auth/routes.py`
- Create: `services/api-gateway/auth/jwt.py`
- Create: `services/api-gateway/auth/middleware.py`
- Create: `services/api-gateway/config.py`
- Create: `tests/services/test_api_auth.py`
**Step 1: Create FastAPI application shell**
`main.py` FastAPI app with CORS middleware (restricted origin), lifespan handler for DB/Redis connections, OpenTelemetry instrumentation (`opentelemetry-instrumentation-fastapi`).
**Step 2: Implement passkey registration (sign up)**
`POST /auth/register/begin` Generate WebAuthn registration options (challenge, relying party info). Uses `py-webauthn`'s `generate_registration_options`.
`POST /auth/register/complete` Verify registration response, store credential public key in `user_credentials` table. Uses `verify_registration_response`.
**Step 3: Implement passkey authentication (sign in)**
`POST /auth/login/begin` Generate authentication options with stored credential IDs.
`POST /auth/login/complete` Verify authentication response, update sign count, issue JWT (access token + refresh token).
**Step 4: Create JWT helper**
Issue short-lived access tokens (15 min) and longer refresh tokens (7 days). `POST /auth/refresh` to get new access token.
**Step 5: Create auth middleware**
FastAPI dependency that extracts and validates JWT from `Authorization: Bearer <token>` header. Skip for `/auth/*`, `/metrics`, `/health` routes.
**Step 6: Write tests**
Test registration flow, authentication flow (mock WebAuthn verification), JWT issuance/validation, middleware rejection of invalid tokens.
**Step 7: Commit**
```bash
git add services/api-gateway/ tests/services/test_api_auth.py
git commit -m "feat: API gateway with passkey (WebAuthn) authentication"
```
---
### Task 14: API Gateway — Trading Endpoints
**Files:**
- Modify: `services/api-gateway/main.py`
- Create: `services/api-gateway/routes/__init__.py`
- Create: `services/api-gateway/routes/portfolio.py`
- Create: `services/api-gateway/routes/trades.py`
- Create: `services/api-gateway/routes/signals.py`
- Create: `services/api-gateway/routes/strategies.py`
- Create: `services/api-gateway/routes/news.py`
- Create: `services/api-gateway/routes/controls.py`
- Create: `services/api-gateway/routes/backtest.py`
- Create: `services/api-gateway/ws.py`
- Create: `tests/services/test_api_routes.py`
**Step 1: Portfolio endpoints**
- `GET /api/portfolio` Current portfolio value, cash, buying power, daily P&L
- `GET /api/portfolio/positions` All open positions with unrealized P&L
- `GET /api/portfolio/history` Equity curve from `portfolio_snapshots` (query params: period)
**Step 2: Trade endpoints**
- `GET /api/trades` Paginated trade history with filters (ticker, date range, strategy, profitable)
- `GET /api/trades/:id` Single trade detail with linked signal, news context, outcome
**Step 3: Signal & strategy endpoints**
- `GET /api/signals` Recent signals with filters
- `GET /api/strategies` All strategies with current weights
- `GET /api/strategies/:id/history` Weight history and adjustments log
- `GET /api/strategies/:id/metrics` Performance metrics over time
**Step 4: News endpoints**
- `GET /api/news` Recent scored articles with filters (ticker, source, score range)
**Step 5: Control endpoints**
- `POST /api/controls/pause` Pause trading (sets flag in Redis checked by trade executor)
- `POST /api/controls/resume` Resume trading
- `POST /api/controls/close-position` Force close a position by ticker
- `GET /api/controls/status` Current trading status (active/paused)
**Step 6: Backtest endpoints**
- `POST /api/backtest/run` Start a backtest with config (date range, capital, strategies, weights). Runs in background task. Returns run_id.
- `GET /api/backtest/:run_id` Get backtest results (status, metrics, trade log, equity curve)
**Step 7: WebSocket endpoint**
`/ws` Authenticated WebSocket. Pushes real-time events: trade executions, new signals, portfolio value updates, news sentiment scores. Uses Redis pub/sub or polling to pick up events from other services.
**Step 8: Write tests**
Test each endpoint group with test client, mock DB queries, verify response schemas.
**Step 9: Commit**
```bash
git add services/api-gateway/ tests/services/test_api_routes.py
git commit -m "feat: API gateway trading endpoints, controls, backtest, WebSocket"
```
---
### Task 15: Dashboard — Project Setup & Auth
**Files:**
- Create: `dashboard/package.json`
- Create: `dashboard/tsconfig.json`
- Create: `dashboard/vite.config.ts`
- Create: `dashboard/tailwind.config.ts`
- Create: `dashboard/postcss.config.js`
- Create: `dashboard/index.html`
- Create: `dashboard/src/main.tsx`
- Create: `dashboard/src/App.tsx`
- Create: `dashboard/src/api/client.ts`
- Create: `dashboard/src/api/auth.ts`
- Create: `dashboard/src/pages/Login.tsx`
- Create: `dashboard/src/pages/Register.tsx`
- Create: `dashboard/src/hooks/useAuth.ts`
- Create: `dashboard/src/components/ProtectedRoute.tsx`
**Step 1: Scaffold React project**
Vite + React 18 + TypeScript. Install: `@tanstack/react-query`, `react-router-dom`, `tailwindcss`, `@simplewebauthn/browser`, `lightweight-charts` (TradingView), `recharts`.
**Step 2: Create API client**
Axios or fetch wrapper with JWT interceptor (auto-attach token, auto-refresh on 401).
**Step 3: Create auth pages**
Registration page: username input calls `/auth/register/begin` triggers browser passkey creation sends attestation to `/auth/register/complete`.
Login page: username input calls `/auth/login/begin` triggers browser passkey assertion sends to `/auth/login/complete` stores JWT.
**Step 4: Create protected route wrapper**
Redirects to login if no valid token. Wraps all dashboard routes.
**Step 5: Commit**
```bash
git add dashboard/
git commit -m "feat: dashboard setup with passkey authentication"
```
---
### Task 16: Dashboard — Trading Views
**Files:**
- Create: `dashboard/src/pages/Portfolio.tsx`
- Create: `dashboard/src/pages/TradeLog.tsx`
- Create: `dashboard/src/pages/Strategies.tsx`
- Create: `dashboard/src/pages/NewsFeed.tsx`
- Create: `dashboard/src/pages/Backtest.tsx`
- Create: `dashboard/src/components/EquityCurve.tsx`
- Create: `dashboard/src/components/PositionsTable.tsx`
- Create: `dashboard/src/components/TradeDetail.tsx`
- Create: `dashboard/src/components/StrategyWeights.tsx`
- Create: `dashboard/src/components/SentimentCard.tsx`
- Create: `dashboard/src/components/Layout.tsx`
- Create: `dashboard/src/hooks/useWebSocket.ts`
- Create: `dashboard/src/hooks/usePortfolio.ts`
**Step 1: Create layout and navigation**
Sidebar nav with links to all 5 views. Top bar with portfolio value summary and trading status indicator (active/paused).
**Step 2: Portfolio Overview page**
- Portfolio value card with daily P&L (green/red)
- Equity curve chart (TradingView lightweight-charts) from `/api/portfolio/history`
- Open positions table with unrealized P&L, close button per position
- Key metrics row: ROI, Sharpe, win rate, max drawdown
**Step 3: Trade Log page**
- Paginated table from `/api/trades`
- Filters: ticker search, date range, strategy dropdown, profitable toggle
- Expandable rows showing linked news, signal details, strategy attribution
**Step 4: Strategy Performance page**
- Strategy cards showing name, current weight, win rate, total P&L
- Weight allocation donut chart (Recharts)
- Weight history line chart per strategy
- Adjustments log table
**Step 5: News & Sentiment Feed page**
- Live feed of scored articles from `/api/news`
- Ticker filter
- Color-coded sentiment badges (green/yellow/red)
- Sparkline of recent sentiment per ticker
**Step 6: Backtesting page**
- Config form: date range pickers, initial capital, strategy checkboxes with weight sliders, slippage/commission inputs
- Submit shows progress displays results dashboard
- Results: equity curve, metrics summary, trade log, per-strategy breakdown
**Step 7: WebSocket hook for real-time updates**
`useWebSocket` hook connects to `/ws`, dispatches events to TanStack Query cache invalidation. Toast notifications for trade executions.
**Step 8: Commit**
```bash
git add dashboard/src/
git commit -m "feat: dashboard trading views — portfolio, trades, strategies, news, backtest"
```
---
## Phase 7: Containerization & Integration
### Task 17: Dockerfiles & Full Docker Compose
**Files:**
- Create: `docker/Dockerfile.service` (multi-stage, shared for all Python services)
- Create: `docker/Dockerfile.dashboard`
- Modify: `docker-compose.yml`
**Step 1: Create Python service Dockerfile**
Multi-stage build. Stage 1: install deps from pyproject.toml with appropriate extras. Stage 2: slim runtime image. Configurable via build arg which service to run and which extras to install.
```dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml .
ARG EXTRAS="api"
RUN pip install --no-cache-dir ".[${EXTRAS}]"
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY shared/ shared/
ARG SERVICE_DIR
COPY services/${SERVICE_DIR}/ service/
CMD ["python", "-m", "service.main"]
```
**Step 2: Create dashboard Dockerfile**
Multi-stage: Node build stage nginx serving static files.
**Step 3: Update docker-compose.yml with all services**
Add all 6 Python services + dashboard container. Each service gets:
- Correct build args (EXTRAS, SERVICE_DIR)
- Depends-on for postgres, redis
- Environment variables from `.env`
- Health check
- Metrics port exposed for Prometheus scraping
**Step 4: Write a smoke test script**
`scripts/smoke-test.sh` Boots full stack, waits for health checks, hits key endpoints, verifies non-error responses.
**Step 5: Commit**
```bash
git add docker/ docker-compose.yml scripts/
git commit -m "feat: dockerfiles and full docker-compose orchestration"
```
---
### Task 18: Integration Testing & Seed Data
**Files:**
- Create: `tests/integration/test_news_pipeline.py`
- Create: `tests/integration/test_trading_flow.py`
- Create: `scripts/seed_strategies.py`
**Step 1: Seed default strategies**
Script to insert the 3 strategies (momentum, mean_reversion, news_driven) with equal initial weights (0.333 each) into the `strategies` table.
**Step 2: Write news pipeline integration test**
Test the full flow: publish a mock RSS article news fetcher picks it up sentiment analyzer scores it signal generator evaluates it. Uses real Redis + PostgreSQL (from docker-compose), mocked Alpaca and FinBERT.
**Step 3: Write trading flow integration test**
Test: inject a signal trade executor processes it (mocked Alpaca broker) trade recorded in DB learning engine evaluates outcome.
**Step 4: Commit**
```bash
git add tests/integration/ scripts/seed_strategies.py
git commit -m "feat: integration tests and strategy seed data"
```
---
## Task Dependencies
```
Task 1 (foundation) ──┬── Task 2 (docker infra)
├── Task 3 (models) ── Task 4 (schemas)
│ │
│ ┌───────────┤
│ │ │
│ Task 5 (broker) │
│ │ │
│ ┌────────┤ Task 6 (news fetcher)
│ │ │ │
│ │ Task 8 (strategies) Task 7 (sentiment)
│ │ │ │
│ │ Task 9 (signal gen) ─┘
│ │ │
│ │ Task 10 (executor)
│ │ │
│ │ Task 11 (learning)
│ │ │
│ │ Task 12 (backtester)
│ │
│ └── Task 13 (API auth) ── Task 14 (API routes)
│ │
│ Task 15 (dashboard setup)
│ │
│ Task 16 (dashboard views)
└── Task 17 (docker) ── Task 18 (integration)
```
**Parallelizable pairs:**
- Task 2 + Task 3 (infra + models)
- Task 5 + Task 6 (broker + news fetcher) after Task 4
- Task 7 + Task 8 (sentiment + strategies) after Task 4
- Task 13 + Task 9 (API auth + signal gen) after their deps
- Task 15 + Task 12 (dashboard setup + backtester) after their deps