trading/docs/plans/2026-02-22-trading-bot-implementation.md

1150 lines
40 KiB
Markdown
Raw Normal View History

# 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