Initial commit: invest-copilot app
This commit is contained in:
@@ -0,0 +1,612 @@
|
||||
# Invest Copilot — Phase 1: Design-Driven Foundation
|
||||
|
||||
> **Goal**: Establish the project foundation, generate UI directions via Open Design, set up the development environment, and define all API contracts.
|
||||
>
|
||||
> **Duration**: Weeks 1-3
|
||||
>
|
||||
> **Deliverables**: Clickable UI prototype, API specification, deployed dev environment, CI/CD pipeline
|
||||
|
||||
## Week 1: Visual Identity & UI Prototypes
|
||||
|
||||
### Day 1-2: Design Direction via Open Design
|
||||
|
||||
**Task 1: Generate 3 visual directions**
|
||||
|
||||
Use Open Design (or equivalent) with these prompts:
|
||||
|
||||
```
|
||||
Direction A: "Dark Finance Professional"
|
||||
- Deep navy background (#0a1628) with electric blue accents (#3b82f6)
|
||||
- Data-dense layout (like Bloomberg but modern)
|
||||
- Monospace numbers (JetBrains Mono), sans-serif labels (Inter)
|
||||
- Green/red for gains/losses with +/− icons (not color alone)
|
||||
- Reference: Bloomberg meets Linear
|
||||
|
||||
Direction B: "Minimal Intelligence"
|
||||
- Near-black background (#0d0d0d) with subtle blue tints
|
||||
- Maximum white space between data elements
|
||||
- Large typography for key metrics
|
||||
- Charts as the hero element
|
||||
- Reference: Apple Health meets Koyfin
|
||||
|
||||
Direction C: "Modern Terminal"
|
||||
- Dark gray background (#1a1a1a) with accent colors
|
||||
- Terminal-inspired typography and borders
|
||||
- Monospace throughout
|
||||
- Command-like navigation
|
||||
- Reference: TradingView dark mode meets VS Code
|
||||
```
|
||||
|
||||
**Task 2: Select direction and create design tokens**
|
||||
|
||||
Based on the generated directions, create `tailwind.config.ts` with:
|
||||
- Custom color palette (OKLch-based)
|
||||
- Font families (Inter + JetBrains Mono)
|
||||
- Spacing scale
|
||||
- Breakpoint definitions
|
||||
- Animation definitions
|
||||
|
||||
**Task 3: Generate key screen prototypes**
|
||||
|
||||
Generate HTML/CSS prototypes for:
|
||||
1. Dashboard (watchlist overview)
|
||||
2. Stock profile (search → profile)
|
||||
3. Sector rotation dashboard
|
||||
|
||||
### Day 3-5: Component Library
|
||||
|
||||
Generate reusable components:
|
||||
- StockCard, PriceChart placeholder, SEC Filing Card
|
||||
- Insider Trade Row, Strategy Rule Builder, Alert Badge
|
||||
- Sector Heatmap, Watchlist Table, Navigation
|
||||
|
||||
## Week 2: Development Environment
|
||||
|
||||
### Task 4: Initialize Next.js 15 project
|
||||
|
||||
```bash
|
||||
npx create-next-app@latest invest-copilot \
|
||||
--typescript \
|
||||
--tailwind \
|
||||
--app \
|
||||
--src-dir \
|
||||
--import-alias "@/*" \
|
||||
--turbopack \
|
||||
--use-npm
|
||||
```
|
||||
|
||||
### Task 5: Install core dependencies
|
||||
|
||||
```bash
|
||||
# State management
|
||||
npm install zustand
|
||||
|
||||
# UI primitives
|
||||
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
|
||||
npm install @radix-ui/react-tabs @radix-ui/react-tooltip
|
||||
npm install @radix-ui/react-select @radix-ui/react-switch
|
||||
|
||||
# Charts
|
||||
npm install lightweight-charts
|
||||
|
||||
# Data fetching
|
||||
npm install @tanstack/react-query
|
||||
|
||||
# Forms
|
||||
npm install react-hook-form zod @hookform/resolvers
|
||||
|
||||
# PWA
|
||||
npm install next-pwa
|
||||
|
||||
# Icons
|
||||
npm install lucide-react
|
||||
|
||||
# Date handling
|
||||
npm install date-fns
|
||||
|
||||
# Development
|
||||
npm install -D @types/node @types/react @types/react-dom
|
||||
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
|
||||
npm install -D prettier prettier-plugin-tailwindcss
|
||||
```
|
||||
|
||||
### Task 6: Set up Docker Compose for infrastructure
|
||||
|
||||
Create `docker-compose.dev.yml`:
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: timescale/timescaledb:latest-pg16
|
||||
environment:
|
||||
POSTGRES_DB: invest_copilot
|
||||
POSTGRES_USER: dev
|
||||
POSTGRES_PASSWORD: dev_password
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: dev
|
||||
MINIO_ROOT_PASSWORD: dev_password
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- miniodata:/data
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
miniodata:
|
||||
```
|
||||
|
||||
### Task 7: Set up CI/CD with GitHub Actions
|
||||
|
||||
Create `.github/workflows/ci.yml`:
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
```
|
||||
|
||||
## Week 3: API Contracts & Data Pipeline
|
||||
|
||||
### Task 8: Define OpenAPI/Swagger specification
|
||||
|
||||
Create `docs/api/openapi.yaml` with:
|
||||
|
||||
```yaml
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Invest Copilot API
|
||||
version: 0.1.0
|
||||
description: AI-native investment research and portfolio copilot
|
||||
|
||||
paths:
|
||||
# Stock Search & Profile
|
||||
/api/v1/search:
|
||||
get:
|
||||
summary: Search stocks by ticker or name
|
||||
parameters:
|
||||
- name: q
|
||||
in: query
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- name: limit
|
||||
in: query
|
||||
schema: { type: integer, default: 10, maximum: 50 }
|
||||
responses:
|
||||
200:
|
||||
description: Search results
|
||||
|
||||
/api/v1/stocks/{ticker}:
|
||||
get:
|
||||
summary: Get full stock profile
|
||||
parameters:
|
||||
- name: ticker
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
200:
|
||||
description: Stock profile with all data
|
||||
|
||||
/api/v1/stocks/{ticker}/peers:
|
||||
get:
|
||||
summary: Get peer group and relative performance
|
||||
parameters:
|
||||
- name: ticker
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
200:
|
||||
description: Peer group with relative strength
|
||||
|
||||
/api/v1/stocks/{ticker}/price/history:
|
||||
get:
|
||||
summary: Get historical prices
|
||||
parameters:
|
||||
- name: ticker
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- name: interval
|
||||
in: query
|
||||
schema: { type: string, enum: [1m, 5m, 15m, 30m, 1h, 1d, 1w, 1M] }
|
||||
- name: start
|
||||
in: query
|
||||
schema: { type: string, format: date-time }
|
||||
- name: end
|
||||
in: query
|
||||
schema: { type: string, format: date-time }
|
||||
responses:
|
||||
200:
|
||||
description: Price history
|
||||
|
||||
/api/v1/stocks/{ticker}/sec-filings:
|
||||
get:
|
||||
summary: Get SEC filings
|
||||
parameters:
|
||||
- name: ticker
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- name: form_type
|
||||
in: query
|
||||
schema: { type: string, enum: [10-K, 10-Q, 8-K, 4, 13F, 13D, 13G] }
|
||||
- name: limit
|
||||
in: query
|
||||
schema: { type: integer, default: 20 }
|
||||
responses:
|
||||
200:
|
||||
description: SEC filings
|
||||
|
||||
/api/v1/stocks/{ticker}/insider-trades:
|
||||
get:
|
||||
summary: Get insider trades
|
||||
parameters:
|
||||
- name: ticker
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- name: limit
|
||||
in: query
|
||||
schema: { type: integer, default: 20 }
|
||||
responses:
|
||||
200:
|
||||
description: Insider trades
|
||||
|
||||
/api/v1/stocks/{ticker}/sentiment:
|
||||
get:
|
||||
summary: Get sentiment signals
|
||||
parameters:
|
||||
- name: ticker
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
200:
|
||||
description: Sentiment signals from all sources
|
||||
|
||||
# Watchlists
|
||||
/api/v1/watchlists:
|
||||
get:
|
||||
summary: Get user's watchlists
|
||||
responses:
|
||||
200:
|
||||
description: List of watchlists
|
||||
post:
|
||||
summary: Create a new watchlist
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string }
|
||||
description: { type: string }
|
||||
responses:
|
||||
201:
|
||||
description: Watchlist created
|
||||
|
||||
/api/v1/watchlists/{id}:
|
||||
get:
|
||||
summary: Get watchlist with items and prices
|
||||
put:
|
||||
summary: Update watchlist name
|
||||
delete:
|
||||
summary: Delete watchlist
|
||||
|
||||
/api/v1/watchlists/{id}/items:
|
||||
get:
|
||||
summary: Get watchlist items with live prices
|
||||
post:
|
||||
summary: Add ticker to watchlist
|
||||
delete:
|
||||
summary: Remove ticker from watchlist
|
||||
|
||||
# Strategies
|
||||
/api/v1/strategies:
|
||||
get:
|
||||
summary: Get user's strategies
|
||||
post:
|
||||
summary: Create a new strategy
|
||||
/api/v1/strategies/{id}:
|
||||
get:
|
||||
summary: Get strategy details
|
||||
put:
|
||||
summary: Update strategy
|
||||
delete:
|
||||
summary: Delete strategy
|
||||
|
||||
# Screener
|
||||
/api/v1/screeners:
|
||||
get:
|
||||
summary: Get user's screeners
|
||||
post:
|
||||
summary: Create a new screener
|
||||
/api/v1/screeners/{id}/run:
|
||||
post:
|
||||
summary: Run screener and get results
|
||||
/api/v1/screeners/{id}/results:
|
||||
get:
|
||||
summary: Get screener results
|
||||
|
||||
# Sector Rotation
|
||||
/api/v1/sectors/rotation:
|
||||
get:
|
||||
summary: Get current sector rotation analysis
|
||||
post:
|
||||
summary: Trigger manual rotation scan
|
||||
/api/v1/sectors/rotation/history:
|
||||
get:
|
||||
summary: Get historical rotation data
|
||||
|
||||
# Alerts
|
||||
/api/v1/alerts:
|
||||
get:
|
||||
summary: Get user's alerts
|
||||
delete:
|
||||
summary: Clear resolved alerts
|
||||
/api/v1/alerts/{id}/resolve:
|
||||
post:
|
||||
summary: Mark alert as resolved
|
||||
/api/v1/alerts/{id}/dismiss:
|
||||
post:
|
||||
summary: Dismiss alert
|
||||
|
||||
# Real-time (SSE)
|
||||
/api/v1/stream/alerts:
|
||||
get:
|
||||
summary: Server-Sent Events stream for real-time alerts
|
||||
responses:
|
||||
200:
|
||||
description: SSE stream
|
||||
content:
|
||||
text/event-stream:
|
||||
schema:
|
||||
type: string
|
||||
```
|
||||
|
||||
### Task 9: Set up Python data pipeline
|
||||
|
||||
```bash
|
||||
mkdir -p src/data-pipeline
|
||||
cd src/data-pipeline
|
||||
pip install yfinance finnhub-python requests beautifulsoup4
|
||||
```
|
||||
|
||||
Create `src/data-pipeline/ingest_prices.py`:
|
||||
```python
|
||||
"""Price data ingestion using yfinance (free tier)."""
|
||||
import yfinance as yf
|
||||
import psycopg2
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def fetch_prices(ticker: str, period: str = "5y") -> list[dict]:
|
||||
"""Fetch OHLCV data using yfinance."""
|
||||
tk = yf.Ticker(ticker)
|
||||
df = tk.history(period=period, auto_adjust=True)
|
||||
records = []
|
||||
for date, row in df.iterrows():
|
||||
records.append({
|
||||
"ticker": ticker,
|
||||
"date": date,
|
||||
"open": row["Open"],
|
||||
"high": row["High"],
|
||||
"low": row["Low"],
|
||||
"close": row["Close"],
|
||||
"volume": int(row["Volume"]),
|
||||
"adjusted_close": row["Close"], # adjusted = close for now
|
||||
})
|
||||
return records
|
||||
|
||||
def save_prices(records: list[dict]) -> None:
|
||||
"""Save prices to PostgreSQL/TimescaleDB."""
|
||||
conn = psycopg2.connect(
|
||||
host="localhost",
|
||||
database="invest_copilot",
|
||||
user="dev",
|
||||
password="dev_password"
|
||||
)
|
||||
cur = conn.cursor()
|
||||
for r in records:
|
||||
cur.execute("""
|
||||
INSERT INTO prices (ticker, date, open, high, low, close, volume, adjusted_close)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (ticker, date) DO UPDATE SET
|
||||
open = EXCLUDED.open,
|
||||
high = EXCLUDED.high,
|
||||
low = EXCLUDED.low,
|
||||
close = EXCLUDED.close,
|
||||
volume = EXCLUDED.volume,
|
||||
adjusted_close = EXCLUDED.adjusted_close
|
||||
""", (
|
||||
r["ticker"], r["date"], r["open"], r["high"], r["low"],
|
||||
r["close"], r["volume"], r["adjusted_close"]
|
||||
))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
tickers = sys.argv[1:] if len(sys.argv) > 1 else ["AAPL", "MSFT", "GOOGL", "AMZN", "META"]
|
||||
for ticker in tickers:
|
||||
print(f"Ingesting {ticker}...")
|
||||
records = fetch_prices(ticker)
|
||||
save_prices(records)
|
||||
print(f" Saved {len(records)} records")
|
||||
```
|
||||
|
||||
### Task 10: Set up SEC EDGAR ingestion
|
||||
|
||||
Create `src/data-pipeline/ingest_sec.py`:
|
||||
```python
|
||||
"""SEC EDGAR filing ingestion."""
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
EDGAR_BASE = "https://data.sec.gov"
|
||||
|
||||
def get_cik(ticker: str) -> str | None:
|
||||
"""Get CIK number for a ticker."""
|
||||
url = f"{EDGAR_BASE}/cgi-bin/browse-edgar?action=getcompany&CIK=TICKER&type=&date=coverage&date_range=365&count=1"
|
||||
url = url.replace("TICKER", ticker.upper())
|
||||
resp = requests.get(url, timeout=10)
|
||||
# Parse HTML to extract CIK
|
||||
import re
|
||||
match = re.search(r'CIK=(0{0,10}\d{6,10})', resp.text)
|
||||
return match.group(1) if match else None
|
||||
|
||||
def get_filings(cik: str, form_type: str | None = None, limit: int = 20) -> list[dict]:
|
||||
"""Get filings for a CIK."""
|
||||
url = f"{EDGAR_BASE}/submissions/CIK{int(cik):010d}.json"
|
||||
resp = requests.get(url, timeout=10)
|
||||
data = resp.json()
|
||||
|
||||
filings = []
|
||||
for i in range(min(limit * 20, len(data["filings"]["recent"]["form"]))):
|
||||
form = data["filings"]["recent"]["form"][i]
|
||||
if form_type and form != form_type:
|
||||
continue
|
||||
filings.append({
|
||||
"cik": cik,
|
||||
"form_type": form,
|
||||
"filing_date": data["filings"]["recent"]["filingDate"][i],
|
||||
"report_date": data["filings"]["recent"]["reportDate"][i],
|
||||
"accession_number": data["filings"]["recent"]["accessionNumber"][i],
|
||||
"primary_document": data["filings"]["recent"]["primaryDocument"][i],
|
||||
"url": f"{EDGAR_BASE}/Archives/edgar/data/{int(cik):010d}/"
|
||||
f"{data['filings']['recent']['accessionNumber'][i].replace('-', '')}/"
|
||||
f"{data['filings']['recent']['primaryDocument'][i]}",
|
||||
})
|
||||
if len([f for f in filings if "form_type" in f]) >= limit:
|
||||
break
|
||||
return filings
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test: Get filings for AAPL
|
||||
cik = get_cik("AAPL")
|
||||
print(f"AAPL CIK: {cik}")
|
||||
if cik:
|
||||
filings = get_filings(cik, limit=5)
|
||||
print(json.dumps(filings, indent=2))
|
||||
```
|
||||
|
||||
### Task 11: Create project structure
|
||||
|
||||
```
|
||||
invest-copilot/
|
||||
├── docs/ # Documentation (we already have this)
|
||||
│ ├── 00-vision.md
|
||||
│ ├── 01-architecture.md
|
||||
│ ├── 02-data-model.md
|
||||
│ ├── 03-domain-model.md
|
||||
│ ├── 04-open-design-integration.md
|
||||
│ ├── 05-phase1-plan.md
|
||||
│ └── api/
|
||||
│ └── openapi.yaml
|
||||
├── src/
|
||||
│ ├── frontend/ # Next.js app
|
||||
│ │ ├── app/
|
||||
│ │ ├── components/
|
||||
│ │ ├── lib/
|
||||
│ │ ├── styles/
|
||||
│ │ └── store/ # Zustand stores
|
||||
│ ├── backend/ # Node.js API (future)
|
||||
│ │ ├── src/
|
||||
│ │ └── tests/
|
||||
│ ├── data-pipeline/ # Python ingestion scripts
|
||||
│ │ ├── ingest_prices.py
|
||||
│ │ ├── ingest_sec.py
|
||||
│ │ ├── ingest_sentiment.py
|
||||
│ │ └── sector_rotation.py
|
||||
│ └── shared/ # Shared types (TypeScript)
|
||||
│ └── types.ts
|
||||
├── docker-compose.dev.yml
|
||||
├── docker-compose.prod.yml # (future)
|
||||
├── .github/
|
||||
│ └── workflows/
|
||||
│ ├── ci.yml
|
||||
│ └── deploy.yml # (future)
|
||||
├── prisma/ # (future, for DDD repo implementations)
|
||||
├── .eslintrc.json
|
||||
├── .prettierrc
|
||||
├── next.config.mjs
|
||||
├── tailwind.config.ts
|
||||
├── tsconfig.json
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Acceptance Criteria for Phase 1
|
||||
|
||||
- [ ] 3 visual directions generated via Open Design (or manual)
|
||||
- [ ] Selected design direction converted to TailwindCSS design tokens
|
||||
- [ ] Next.js 15 project running with TypeScript, TailwindCSS, Radix UI
|
||||
- [ ] Docker Compose dev environment with PostgreSQL+TimescaleDB, Redis, MinIO
|
||||
- [ ] GitHub Actions CI pipeline (lint + build)
|
||||
- [ ] Complete OpenAPI specification for all endpoints
|
||||
- [ ] Python data pipeline ingesting prices for at least 10 tickers
|
||||
- [ ] SEC EDGAR ingestion working for at least 5 companies
|
||||
- [ ] Dashboard UI mockup working with static data
|
||||
- [ ] Stock search UI mockup working with static data
|
||||
- [ ] Stock profile UI mockup working with static data
|
||||
- [ ] README.md with project overview, setup instructions, and architecture
|
||||
|
||||
## Pre-Phase 2 Checklist
|
||||
|
||||
Before moving to Phase 2 (Backend Foundation):
|
||||
- [ ] All Phase 1 acceptance criteria met
|
||||
- [ ] Design direction finalized and approved
|
||||
- [ ] API contracts reviewed and agreed upon
|
||||
- [ ] Data pipeline tested and verified
|
||||
- [ ] Dev environment stable and documented
|
||||
|
||||
## Resource Estimation
|
||||
|
||||
| Task | Effort |
|
||||
|---|---|
|
||||
| Visual directions + design tokens | 2 days |
|
||||
| Component library | 3 days |
|
||||
| Next.js project setup | 0.5 days |
|
||||
| Docker Compose infrastructure | 0.5 days |
|
||||
| CI/CD pipeline | 0.5 days |
|
||||
| OpenAPI specification | 1 day |
|
||||
| Price data pipeline | 2 days |
|
||||
| SEC EDGAR pipeline | 2 days |
|
||||
| UI mockups (3 screens) | 3 days |
|
||||
| README + documentation | 1 day |
|
||||
| **Total** | **~16 days (3 weeks)** |
|
||||
Reference in New Issue
Block a user