Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/agents/codewhale-reviwer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: codewhale-reviwer
description: This agent reviews code comprehensively and provides feedback on potential improvements, best practices, and optimizations using deepseek and codewhale.
---

You are using a different AI agent to carry out a review of the file planning/PLAN.md. You will write your feedback in planning/review-deepseek.md. You will use the deepseek API to analyze the code and provide feedback on potential improvements, best practices, and optimizations.
You must execute the following shell command to carry out the review:

`codewhale exec --auto "Please review all the changes since last commit and write your feedback in planning/review-deepseek.md"`

write you feedback in planning/review-deepseek.md
This will run the review and save the results.
DO NOT RUN THE REVIEW YOURSELF.
6 changes: 6 additions & 0 deletions .claude/agents/reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
name: reviewer
description: this agent reviews code comprehensively and provides feedback on potential improvements, best practices, and optimizations.
---

You review the file planning/PLAN.md and write your feedback in planning/review-claude.md
1 change: 1 addition & 0 deletions .claude/commands/doc-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Review the documentation in the planning folder called $ARGUEENTS and add questions, clarifications or feedback to a new section at the end along with any opportunities to simplify
Empty file.
71 changes: 35 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,61 @@
# FinAlly — AI Trading Workstation

A visually stunning AI-powered trading workstation that streams live market data, simulates portfolio trading, and integrates an LLM chat assistant that can analyze positions and execute trades via natural language.
FinAlly (Finance Ally) is a visually stunning, AI-powered trading workstation: it streams live market data, lets you trade a simulated portfolio, and includes an LLM chat assistant that can analyze your positions and execute trades on your behalf. Think Bloomberg terminal with an AI copilot.

Built entirely by coding agents as a capstone project for an agentic AI coding course.
This is the capstone project for an agentic AI coding course — built entirely by coding agents to demonstrate how orchestrated AI agents can produce a production-quality full-stack application.

## Features
## Status

- **Live price streaming** via SSE with green/red flash animations
- **Simulated portfolio** — $10k virtual cash, market orders, instant fills
- **Portfolio visualizations** — heatmap (treemap), P&L chart, positions table
- **AI chat assistant** — analyzes holdings, suggests and auto-executes trades
- **Watchlist management** — track tickers manually or via AI
- **Dark terminal aesthetic** — Bloomberg-inspired, data-dense layout
The project is under active development. The **market data subsystem** (live-price simulator, Massive/Polygon.io client, price cache, SSE streaming) is complete and lives in `backend/app/market/`. The rest of the platform — portfolio/trading, watchlist, LLM chat, frontend, and Docker packaging — is still to be built.

See [`planning/PLAN.md`](planning/PLAN.md) for the full project specification, and [`planning/MARKET_DATA_SUMMARY.md`](planning/MARKET_DATA_SUMMARY.md) for details on the completed market data component.

## Architecture

Single Docker container serving everything on port 8000:
A single Docker container, single port (8000):

- **Frontend**: Next.js + TypeScript, built as a static export, served by FastAPI
- **Backend**: FastAPI (Python), managed with `uv`
- **Database**: SQLite, volume-mounted for persistence
- **Real-time data**: Server-Sent Events (SSE)
- **AI**: LiteLLM → OpenRouter (Cerebras inference), structured outputs for trade execution
- **Market data**: simulator by default, real data via the Massive API if a key is provided

- **Frontend**: Next.js (static export) with TypeScript and Tailwind CSS
- **Backend**: FastAPI (Python/uv) with SSE streaming
- **Database**: SQLite with lazy initialization
- **AI**: LiteLLM → OpenRouter (Cerebras inference) with structured outputs
- **Market data**: Built-in GBM simulator (default) or Massive API (optional)
Full rationale and details are in `planning/PLAN.md`.

## Quick Start
## Getting Started (backend, current state)

```bash
# Clone and configure
cp .env.example .env
# Add your OPENROUTER_API_KEY to .env
cd backend
uv sync --extra dev # install dependencies
uv run --extra dev pytest # run tests
uv run market_data_demo.py # live terminal dashboard with simulated prices
```

# Run with Docker
docker build -t finally .
docker run -v finally-data:/app/db -p 8000:8000 --env-file .env finally
See [`backend/CLAUDE.md`](backend/CLAUDE.md) and [`backend/README.md`](backend/README.md) for backend developer docs.

# Open http://localhost:8000
```
A one-command Docker launch (`scripts/start_mac.sh` / `scripts/start_windows.ps1`) is planned once the frontend and portfolio/chat backend are in place.

## Environment Variables

| Variable | Required | Description |
|---|---|---|
| `OPENROUTER_API_KEY` | Yes | OpenRouter API key for AI chat |
| `MASSIVE_API_KEY` | No | Massive (Polygon.io) key for real market data; omit to use simulator |
| `LLM_MOCK` | No | Set `true` for deterministic mock LLM responses (testing) |
```bash
OPENROUTER_API_KEY= # required for LLM chat
MASSIVE_API_KEY= # optional; enables real market data instead of the simulator
LLM_MOCK=false # set true for deterministic mock LLM responses (testing)
```

## Project Structure

```
finally/
├── frontend/ # Next.js static export
├── backend/ # FastAPI uv project
├── planning/ # Project documentation and agent contracts
├── test/ # Playwright E2E tests
├── db/ # SQLite volume mount (runtime)
└── scripts/ # Start/stop helpers
├── frontend/ # Next.js TypeScript app (static export) — planned
├── backend/ # FastAPI uv project — market data implemented
├── planning/ # Project specification and design docs for agents
├── scripts/ # Docker start/stop scripts — planned
├── test/ # Playwright E2E tests — planned
└── db/ # SQLite volume mount point
```

## License

See [LICENSE](LICENSE).
See [`LICENSE`](LICENSE).
17 changes: 8 additions & 9 deletions backend/app/market/massive_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import logging

from massive import RESTClient
from massive.rest.models import SnapshotMarketType

from .cache import PriceCache
from .interface import MarketDataSource
Expand All @@ -21,8 +20,11 @@ class MassiveDataSource(MarketDataSource):
tickers in a single API call, then writes results to the PriceCache.

Rate limits:
- Free tier: 5 req/min → poll every 15s (default)
- Paid tiers: higher limits → poll every 2-5s
- Free (Basic) tier: does not include this endpoint at all (403) — a
Starter plan or above is required for get_snapshot_all.
- Starter and above: effectively unlimited (soft guidance: stay under
100 req/s), so the poll interval is a design choice rather than a
rate-limit constraint; default here is a conservative 15s.
"""

def __init__(
Expand Down Expand Up @@ -99,8 +101,8 @@ async def _poll_once(self) -> None:
for snap in snapshots:
try:
price = snap.last_trade.price
# Massive timestamps are Unix milliseconds → convert to seconds
timestamp = snap.last_trade.timestamp / 1000.0
# LastTrade timestamps are Unix nanoseconds → convert to seconds
timestamp = snap.last_trade.sip_timestamp / 1_000_000_000
self._cache.update(
ticker=snap.ticker,
price=price,
Expand All @@ -122,7 +124,4 @@ async def _poll_once(self) -> None:

def _fetch_snapshots(self) -> list:
"""Synchronous call to the Massive REST API. Runs in a thread."""
return self._client.get_snapshot_all(
market_type=SnapshotMarketType.STOCKS,
tickers=self._tickers,
)
return self._client.get_snapshot_all("stocks", self._tickers)
16 changes: 8 additions & 8 deletions backend/tests/market/test_massive.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
from app.market.massive_client import MassiveDataSource


def _make_snapshot(ticker: str, price: float, timestamp_ms: int) -> MagicMock:
def _make_snapshot(ticker: str, price: float, timestamp_ns: int) -> MagicMock:
"""Create a mock Massive snapshot object."""
snap = MagicMock()
snap.ticker = ticker
snap.last_trade = MagicMock()
snap.last_trade.price = price
snap.last_trade.timestamp = timestamp_ms
snap.last_trade.sip_timestamp = timestamp_ns
return snap


Expand All @@ -34,8 +34,8 @@ async def test_poll_updates_cache(self):
source._client = MagicMock() # Satisfy the _poll_once guard

mock_snapshots = [
_make_snapshot("AAPL", 190.50, 1707580800000),
_make_snapshot("GOOGL", 175.25, 1707580800000),
_make_snapshot("AAPL", 190.50, 1707580800000000000),
_make_snapshot("GOOGL", 175.25, 1707580800000000000),
]

with patch.object(source, "_fetch_snapshots", return_value=mock_snapshots):
Expand All @@ -55,7 +55,7 @@ async def test_malformed_snapshot_skipped(self):
source._tickers = ["AAPL", "BAD"]
source._client = MagicMock() # Satisfy the _poll_once guard

good_snap = _make_snapshot("AAPL", 190.50, 1707580800000)
good_snap = _make_snapshot("AAPL", 190.50, 1707580800000000000)
bad_snap = MagicMock()
bad_snap.ticker = "BAD"
bad_snap.last_trade = None # Will cause AttributeError
Expand Down Expand Up @@ -84,7 +84,7 @@ async def test_api_error_does_not_crash(self):
assert cache.get_price("AAPL") is None # No update happened

async def test_timestamp_conversion(self):
"""Test that timestamps are converted from milliseconds to seconds."""
"""Test that timestamps are converted from nanoseconds to seconds."""
cache = PriceCache()
source = MassiveDataSource(
api_key="test-key",
Expand All @@ -94,7 +94,7 @@ async def test_timestamp_conversion(self):
source._tickers = ["AAPL"]
source._client = MagicMock() # Satisfy the _poll_once guard

mock_snapshots = [_make_snapshot("AAPL", 190.50, 1707580800000)]
mock_snapshots = [_make_snapshot("AAPL", 190.50, 1707580800000000000)]

with patch.object(source, "_fetch_snapshots", return_value=mock_snapshots):
await source._poll_once()
Expand Down Expand Up @@ -189,7 +189,7 @@ async def test_start_immediate_poll(self):
cache = PriceCache()
source = MassiveDataSource(api_key="test-key", price_cache=cache, poll_interval=60.0)

mock_snapshots = [_make_snapshot("AAPL", 190.50, 1707580800000)]
mock_snapshots = [_make_snapshot("AAPL", 190.50, 1707580800000000000)]

with patch("app.market.massive_client.RESTClient"):
with patch.object(source, "_fetch_snapshots", return_value=mock_snapshots):
Expand Down
5 changes: 5 additions & 0 deletions independent-reviewer/.claude-plugins/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "independent-reviewer",
"description": "carries an indpendent reviews of all changes since last commit",
"version": "0.1.0"
}
14 changes: 14 additions & 0 deletions independent-reviewer/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hooks": {
"stops": [
{
"hooks": [
{
"type": "command",
"command": "codewhale exec auto \"review changes since last commit and suggest improvements and resulst in a doccuments planning/codewhale-reveiw.md\""
}
]
}
]
}
}
Loading