diff --git a/README.md b/README.md index a313ba73..ab03cbad 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ -# QuantResearch +# QuantResearch_Opcode — Updated README -check out the [link](https://qrsopcode.netlify.app/) - -> **QuantResearch** — research-grade quantitative strategy starter kit with an interactive React/TypeScript frontend (cauweb), Python backtesting core, and legacy Streamlit dashboards archived under `legacy/streamlit/`. +> **QuantResearch_Opcode** — research-grade quantitative strategy starter kit with an interactive React/TypeScript frontend (cauweb), Python backtesting core, and legacy Streamlit dashboards archived under `legacy/streamlit/`. --- @@ -210,7 +208,7 @@ The frontend expects a stable WS message contract. A suggested minimal schema (e --- -## APIs & Data flows: to be updated +## APIs & Data flows * **Backtest flow**: Frontend POSTs to `/api/backtest` with strategy config → backend enqueues job → backend emits progress to `backtest:{job_id}` → final results stored in DB and accessible via `/api/backtest/{job_id}/results`. * **Strategy CRUD**: REST endpoints for strategy create / update / delete, with validation in Python core. diff --git a/legacy/streamlit/README.md b/legacy/streamlit/README.md new file mode 100644 index 00000000..363b73ea --- /dev/null +++ b/legacy/streamlit/README.md @@ -0,0 +1,227 @@ +# QuantResearchStarter + +[![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/) +[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[![CI](https://github.com/username/QuantResearchStarter/actions/workflows/ci.yml/badge.svg)](https://github.com/username/QuantResearchStarter/actions) + +A modular, open-source quantitative research and backtesting framework built for clarity, reproducibility, and extensibility. Ideal for researchers, students, and engineers building and testing systematic strategies. + +--- + +## Why this project + +QuantResearchStarter aims to provide a clean, well-documented starting point for quantitative research and backtesting. It focuses on: + +* **Readability**: idiomatic Python, type hints, and small modules you can read and change quickly. +* **Testability**: deterministic vectorized backtests with unit tests and CI. +* **Extensibility**: plug-in friendly factor & data adapters so you can try new ideas fast. + +--- + +## Key features + +* **Data management** — download market data or generate synthetic price series for experiments. +* **Factor library** — example implementations of momentum, value, size, and volatility factors. +* **Vectorized backtesting engine** — supports transaction costs, slippage, portfolio constraints, and configurable rebalancing frequencies (daily, weekly, monthly). +* **Risk & performance analytics** — returns, drawdowns, Sharpe, turnover, and other risk metrics. +* **CLI & scripts** — small tools to generate data, compute factors, and run backtests from the terminal. +* **Production-ready utilities** — type hints, tests, continuous integration, and documentation scaffolding. + +--- + +## Quick start + +### Requirements + +* Python 3.10+ +* pip + +### Install locally + +```bash +# Clone the repository +git clone https://github.com/username/QuantResearchStarter.git +cd QuantResearchStarter + +# Install package in development mode +pip install -e . + +# Install development dependencies (tests, linters, docs) +pip install -e ".[dev]" + +# Optional UI dependencies +pip install streamlit plotly +``` + +### Quick CLI Usage + +After installation, you can use the CLI in two ways: + +**Option 1: Direct command (if PATH is configured)** +```bash +qrs --help +# generate synthetic sample price series +qrs generate-data -o data_sample/sample_prices.csv -s 5 -d 365 +# compute example factors +qrs compute-factors -d data_sample/sample_prices.csv -f momentum -f value -o output/factors.csv +# run a backtest +qrs backtest -d data_sample/sample_prices.csv -s output/factors.csv -o output/backtest_results.json +``` + +**Option 2: Python module (always works)** +```bash +python -m quant_research_starter.cli --help +python -m quant_research_starter.cli generate-data -o data_sample/sample_prices.csv -s 5 -d 365 +python -m quant_research_starter.cli compute-factors -d data_sample/sample_prices.csv -f momentum -f value +python -m quant_research_starter.cli backtest -d data_sample/sample_prices.csv -s output/factors.csv -o output/backtest_results.json +``` + +### Demo (one-line) + +```bash +make demo +``` + +### Step-by-step demo + +```bash +# generate synthetic sample price series +python -m quant_research_starter.cli generate-data -o data_sample/sample_prices.csv -s 5 -d 365 + +# compute example factors +python -m quant_research_starter.cli compute-factors -d data_sample/sample_prices.csv -f momentum -f value -o output/factors.csv + +# run a backtest +python -m quant_research_starter.cli backtest -d data_sample/sample_prices.csv -s output/factors.csv -o output/backtest_results.json + +# DISCLAIMER: OLD VERSION +# optional: start the Streamlit dashboard, if on main stream +streamlit run src/quant_research_starter/dashboard/streamlit_app.py +# NEW VERSION: if streamlit is in legacy folder +streamlit run legacy/streamlit/streamlit_app.py +``` + +--- + +## Example: small strategy (concept) + +```python +from quant_research_starter.backtest import Backtester +from quant_research_starter.data import load_prices +from quant_research_starter.factors import Momentum + +prices = load_prices("data_sample/sample_prices.csv") +factor = Momentum(window=63) +scores = factor.compute(prices) + +bt = Backtester(prices, signals=scores, capital=1_000_000) +results = bt.run() +print(results.performance.summary()) +``` + +### Rebalancing Frequency + +The backtester supports different rebalancing frequencies to match your strategy needs: + +```python +from quant_research_starter.backtest import VectorizedBacktest +# Daily rebalancing (default) +bt_daily = VectorizedBacktest(prices, signals, rebalance_freq="D") + +# Weekly rebalancing (reduces turnover and transaction costs) +bt_weekly = VectorizedBacktest(prices, signals, rebalance_freq="W") + +# Monthly rebalancing (lowest turnover) +bt_monthly = VectorizedBacktest(prices, signals, rebalance_freq="M") + +results = bt_monthly.run() +``` + +Supported frequencies: +- `"D"`: Daily rebalancing (default) +- `"W"`: Weekly rebalancing (rebalances when the week changes) +- `"M"`: Monthly rebalancing (rebalances when the month changes) + +> The code above is illustrative—see `examples/` for fully working notebooks and scripts. + +--- + +## CLI reference + +Run `python -m quant_research_starter.cli --help` or `python -m quant_research_starter.cli --help` for full usage. Main commands include: + +* `python -m quant_research_starter.cli generate-data` — create synthetic price series or download data from adapters +* `python -m quant_research_starter.cli compute-factors` — calculate and export factor scores +* `python -m quant_research_starter.cli backtest` — run the vectorized backtest and export results + +**Note:** If you have the `qrs` command in your PATH, you can use `qrs` instead of `python -m quant_research_starter.cli`. + +--- + +## Project structure (overview) + +``` +QuantResearchStarter/ +├─ src/quant_research_starter/ +│ ├─ data/ # data loaders & adapters +│ ├─ factors/ # factor implementations +│ ├─ backtest/ # backtester & portfolio logic +│ ├─ analytics/ # performance and risk metrics +│ ├─ cli/ # command line entry points +│ └─ dashboard/ # optional Streamlit dashboard +├─ examples/ # runnable notebooks & example strategies +├─ tests/ # unit + integration tests +└─ docs/ # documentation source +``` + +--- + +## Tests & CI + +We include unit tests and a CI workflow (GitHub Actions). Run tests locally with: + +```bash +pytest -q +``` + +The CI pipeline runs linting, unit tests, and builds docs on push/PR. + +--- + +## Contributing + +Contributions are very welcome. Please follow these steps: + +1. Fork the repository +2. Create a feature branch +3. Add tests for new behavior +4. Open a pull request with a clear description and rationale + +Please review `CONTRIBUTING.md` and the `CODE_OF_CONDUCT.md` before submitting. + +--- + +## AI policy — short & practical + +**Yes — you are allowed to use AI tools** (ChatGPT, Copilot, Codeium, etc.) to help develop, prototype, or document code in this repository. + +A few friendly guidelines: + +* **Be transparent** when a contribution is substantially generated by an AI assistant — add a short note in the PR or commit message (e.g., "Generated with ChatGPT; reviewed and adapted by "). +* **Review and test** all AI-generated code. Treat it as a helpful draft, not final production-quality code. +* **Follow licensing** and attribution rules for any external snippets the AI suggests. Don’t paste large verbatim copyrighted material. +* **Security & correctness**: double-check numerical logic, data handling, and anything that affects trading decisions. + +This policy is intentionally permissive: we want the community to move fast while keeping quality and safety in mind. + +--- + +## License + +This project is licensed under the MIT License — see the `LICENSE` file for details. + +--- + +## Acknowledgements + +Built with inspiration from open-source quant libraries and the research community. If you use this project in papers or public work, a short citation or mention is appreciated. diff --git a/src/quant_research_starter/api/alembic/__pycache__/env.cpython-312.pyc b/src/quant_research_starter/api/alembic/__pycache__/env.cpython-312.pyc new file mode 100644 index 00000000..b373e96c Binary files /dev/null and b/src/quant_research_starter/api/alembic/__pycache__/env.cpython-312.pyc differ diff --git a/src/quant_research_starter/api/alembic/env.py b/src/quant_research_starter/api/alembic/env.py new file mode 100644 index 00000000..d464b630 --- /dev/null +++ b/src/quant_research_starter/api/alembic/env.py @@ -0,0 +1,85 @@ +from __future__ import with_statement +import os +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. Some environments (CI or +# trimmed alembic.ini) may not include all logger sections; guard against +# that to avoid stopping migrations with a KeyError. +if config.config_file_name is not None: + try: + fileConfig(config.config_file_name) + except Exception: + # Fall back to a minimal logging configuration if the ini is missing + # expected logger sections (e.g. 'logger_sqlalchemy'). This makes + # migrations resilient when run in different environments. + import logging + + logging.basicConfig(level=logging.INFO) + +target_metadata = None + +# Use DATABASE_URL env if provided +db_url = os.getenv("DATABASE_URL") or config.get_main_option("sqlalchemy.url") +if db_url: + # Only set the option if we have a valid string value. Avoid setting None + # which causes ConfigParser type errors (option values must be strings). + config.set_main_option("sqlalchemy.url", str(db_url)) + + +def run_migrations_offline(): + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + # Determine whether the configured URL uses an async driver. If so, + # create an AsyncEngine and run the migrations inside an async context + # while delegating the actual migration steps to a sync callable via + # `connection.run_sync`. Otherwise, fall back to the classic sync path. + url = config.get_main_option("sqlalchemy.url") + + def _do_run_migrations(connection): + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + if url and url.startswith("postgresql+asyncpg"): + # Async migration path + from sqlalchemy.ext.asyncio import create_async_engine + import asyncio + + async_engine = create_async_engine(url, future=True) + + async def run(): + async with async_engine.connect() as connection: + await connection.run_sync(_do_run_migrations) + + asyncio.run(run()) + else: + # Sync migration path (classic) + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + _do_run_migrations(connection) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/src/quant_research_starter/api/alembic/versions/0001_initial_create_users_and_jobs.py b/src/quant_research_starter/api/alembic/versions/0001_initial_create_users_and_jobs.py new file mode 100644 index 00000000..6b86b792 --- /dev/null +++ b/src/quant_research_starter/api/alembic/versions/0001_initial_create_users_and_jobs.py @@ -0,0 +1,43 @@ +"""initial create users and backtest_jobs tables + +Revision ID: 0001_initial_create_users_and_jobs +Revises: +Create Date: 2025-11-17 +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0001_initial_create_users_and_jobs' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'users', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('username', sa.String(length=128), nullable=False, unique=True, index=True), + sa.Column('hashed_password', sa.String(length=256), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('true')), + sa.Column('role', sa.String(length=32), nullable=False, server_default='user'), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), + ) + + op.create_table( + 'backtest_jobs', + sa.Column('id', sa.String(length=64), primary_key=True), + sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id'), nullable=True), + sa.Column('status', sa.String(length=32), nullable=False, server_default='queued'), + sa.Column('params', sa.JSON(), nullable=True), + sa.Column('result_path', sa.String(length=1024), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), + sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now()), + ) + + +def downgrade(): + op.drop_table('backtest_jobs') + op.drop_table('users') diff --git a/src/quant_research_starter/api/routers/__init__.py b/src/quant_research_starter/api/routers/__init__.py new file mode 100644 index 00000000..6ea05307 --- /dev/null +++ b/src/quant_research_starter/api/routers/__init__.py @@ -0,0 +1,9 @@ +"""API routers package.""" + +from fastapi import APIRouter + +router = APIRouter() + +from . import auth as auth_router # noqa: E402,F401 +from . import backtest as backtest_router # noqa: E402,F401 +from . import assets as assets_router # noqa: E402,F401 diff --git a/src/quant_research_starter/api/routers/assets.py b/src/quant_research_starter/api/routers/assets.py new file mode 100644 index 00000000..b2a0b03b --- /dev/null +++ b/src/quant_research_starter/api/routers/assets.py @@ -0,0 +1,15 @@ +"""Assets router to expose available symbols / sample data.""" +from fastapi import APIRouter +from quant_research_starter.data.sample_loader import SampleDataLoader + +router = APIRouter(prefix="/api/assets", tags=["assets"]) + + +@router.get("/") +async def list_assets(): + loader = SampleDataLoader() + df = loader.load_sample_prices() + symbols = [] + for sym in df.columns: + symbols.append({"symbol": sym, "price": float(df[sym].iloc[-1])}) + return symbols diff --git a/src/quant_research_starter/api/routers/auth.py b/src/quant_research_starter/api/routers/auth.py new file mode 100644 index 00000000..30b5b1cc --- /dev/null +++ b/src/quant_research_starter/api/routers/auth.py @@ -0,0 +1,35 @@ +"""Authentication routes: register and token endpoints.""" +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from fastapi.security import OAuth2PasswordRequestForm + +from .. import schemas, db, models, auth + +router = APIRouter(prefix="/api/auth", tags=["auth"]) + + +@router.post("/register", response_model=schemas.UserRead) +async def register_user(user_in: schemas.UserCreate, session: AsyncSession = Depends(db.get_session)): + q = await session.execute(models.User.__table__.select().where(models.User.username == user_in.username)) + if q.first(): + raise HTTPException(status_code=400, detail="Username already registered") + hashed = auth.get_password_hash(user_in.password) + user = models.User(username=user_in.username, hashed_password=hashed) + session.add(user) + await session.commit() + await session.refresh(user) + return schemas.UserRead(id=user.id, username=user.username, is_active=user.is_active, role=user.role) + + +@router.post("/token", response_model=schemas.Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), session: AsyncSession = Depends(db.get_session)): + q = await session.execute(models.User.__table__.select().where(models.User.username == form_data.username)) + row = q.first() + if not row: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = row[0] + if not auth.verify_password(form_data.password, user.hashed_password): + raise HTTPException(status_code=400, detail="Incorrect username or password") + + access_token = auth.create_access_token(data={"sub": user.username}) + return {"access_token": access_token, "token_type": "bearer"} diff --git a/src/quant_research_starter/api/routers/backtest.py b/src/quant_research_starter/api/routers/backtest.py new file mode 100644 index 00000000..99c76783 --- /dev/null +++ b/src/quant_research_starter/api/routers/backtest.py @@ -0,0 +1,67 @@ +"""Backtest endpoints: enqueue backtest jobs and fetch results.""" +from __future__ import annotations +import uuid +import os +from fastapi import APIRouter, Depends, BackgroundTasks, UploadFile, File, HTTPException, WebSocket +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Optional + +from .. import schemas, db, models, auth +from ..tasks.celery_app import celery_app +from ..utils.ws_manager import manager + +router = APIRouter(prefix="/api/backtest", tags=["backtest"]) + + +@router.post("/", response_model=schemas.BacktestStatus) +async def submit_backtest(req: schemas.BacktestRequest, current_user=Depends(auth.require_active_user), session: AsyncSession = Depends(db.get_session)): + # Create job + job_id = uuid.uuid4().hex + job = models.BacktestJob(id=job_id, user_id=current_user.id, status="queued", params=req.dict()) + session.add(job) + await session.commit() + + # Enqueue celery task + celery_app.send_task("quant_research_starter.api.tasks.tasks.run_backtest", args=[job_id, req.dict()]) + + return {"job_id": job_id, "status": "queued"} + + +@router.get("/{job_id}/results") +async def get_results(job_id: str, current_user=Depends(auth.require_active_user), session: AsyncSession = Depends(db.get_session)): + q = await session.execute(models.BacktestJob.__table__.select().where(models.BacktestJob.id == job_id)) + row = q.first() + if not row: + raise HTTPException(status_code=404, detail="Job not found") + job = row[0] + if job.user_id != current_user.id and current_user.role != "admin": + raise HTTPException(status_code=403, detail="Not authorized to view this job") + + if job.result_path and os.path.exists(job.result_path): + import json + + with open(job.result_path, "r") as f: + return json.load(f) + return {"status": job.status} + + +@router.websocket("/ws/{job_id}") +async def websocket_backtest(websocket: WebSocket, job_id: str): + """WebSocket endpoint that registers the client and relays messages from Redis pub/sub. + + The Redis listener broadcasts messages to the ConnectionManager which then sends + them to connected WebSocket clients. + """ + await manager.connect(job_id, websocket) + try: + while True: + # keep the connection alive; client may send ping messages + msg = await websocket.receive_text() + # ignore incoming messages; server pushes updates + await websocket.send_text("ok") + except Exception: + manager.disconnect(job_id, websocket) + try: + await websocket.close() + except Exception: + pass diff --git a/src/quant_research_starter/frontend/cauweb/package.json b/src/quant_research_starter/frontend/cauweb/package.json index 418c6c33..ddf01798 100644 --- a/src/quant_research_starter/frontend/cauweb/package.json +++ b/src/quant_research_starter/frontend/cauweb/package.json @@ -12,10 +12,10 @@ "@tailwindcss/vite": "^4.1.17", "chart.js": "^4.5.1", "lucide-react": "^0.263.1", - "react": "^18.2.0", + "react": "^19.2.0", "react-chartjs-2": "^5.3.1", - "react-dom": "^18.2.0", - "react-router-dom": "^6.8.0" + "react-dom": "^19.2.0", + "react-router-dom": "^6.30.1" }, "devDependencies": { "@tailwindcss/cli": "^4.1.17", @@ -23,10 +23,10 @@ "@types/react": "^18.3.26", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", - "autoprefixer": "^10.4.21", + "autoprefixer": "^10.4.22", "postcss": "^8.5.6", "tailwindcss": "^4.1.17", "typescript": "^5.9.3", - "vite": "^5.0.0" + "vite": "^5.4.21" } } diff --git a/src/quant_research_starter/frontend/cauweb/pyproject.toml b/src/quant_research_starter/frontend/cauweb/pyproject.toml new file mode 100644 index 00000000..6e81aa4f --- /dev/null +++ b/src/quant_research_starter/frontend/cauweb/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "quant-research-frontend" +version = "0.1.0" +description = "" +authors = [ + {name = "AYUSH KUMAR TIWARI",email = "tiwariayush4373@gmail.com"} +] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/quant_research_starter/frontend/cauweb/src/main.tsx b/src/quant_research_starter/frontend/cauweb/src/main.tsx index 741be130..6012bf27 100644 --- a/src/quant_research_starter/frontend/cauweb/src/main.tsx +++ b/src/quant_research_starter/frontend/cauweb/src/main.tsx @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; + import { App } from './App'; import './styles/globals.css'; // Make sure this import exists import './index.css'; diff --git a/src/quant_research_starter/frontend/metrics/package.json b/src/quant_research_starter/frontend/metrics/package.json deleted file mode 100644 index 366df2e2..00000000 --- a/src/quant_research_starter/frontend/metrics/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "metrics-dashboard", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" - }, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "recharts": "^2.8.0", - "lucide-react": "^0.294.0" - }, - "devDependencies": { - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", - "@vitejs/plugin-react": "^4.1.1", - "autoprefixer": "^10.4.16", - "eslint": "^8.53.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.4", - "postcss": "^8.4.31", - "tailwindcss": "^3.3.5", - "typescript": "^5.2.2", - "vite": "^4.5.0" - } -} \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/postcss.config.js b/src/quant_research_starter/frontend/metrics/postcss.config.js deleted file mode 100644 index e99ebc2c..00000000 --- a/src/quant_research_starter/frontend/metrics/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/src/App.tsx b/src/quant_research_starter/frontend/metrics/src/App.tsx deleted file mode 100644 index 4b003b76..00000000 --- a/src/quant_research_starter/frontend/metrics/src/App.tsx +++ /dev/null @@ -1,130 +0,0 @@ -//import React from 'react'; -import { useBacktestData } from './hooks/useBacktestData'; -import MetricCard from './components/MetricCard'; -import PerformanceChart from './components/PerformanceChart'; -import DrawdownChart from './components/DrawdownChart'; -//import { RefreshCw, AlertCircle, Database } from 'lucide-react'; - -function App() { - const { data, loading, error, refreshData } = useBacktestData(); - - if (error) { - return ( -
-
- -

Error Loading Data

-

{error}

- -
-
- ); - } - - return ( -
- {/* Header */} -
-
-
-
- -

Metrics Dashboard

-
- -
-
-
- - {/* Main Content */} -
- {/* Metrics Grid */} -
- - - - - - -
- - {/* Charts */} -
- - -
- - {/* Loading State */} - {loading && !data && ( -
-
- -

Loading metrics data...

-
-
- )} -
- - {/* Footer */} -
-
-

- Metrics Dashboard • Built with React, TypeScript, and Recharts -

-
-
-
- ); -} - -export default App; \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/src/components/DrawdownChart.tsx b/src/quant_research_starter/frontend/metrics/src/components/DrawdownChart.tsx deleted file mode 100644 index 2db2ab7b..00000000 --- a/src/quant_research_starter/frontend/metrics/src/components/DrawdownChart.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { - AreaChart, - Area, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer -} from 'recharts'; -import { DrawdownDataPoint } from '../types'; -import { formatNumber, formatDate } from '../utils/formatters'; - -interface DrawdownChartProps { - data: DrawdownDataPoint[]; - height?: number; - loading?: boolean; -} - -const DrawdownChart: React.FC = ({ - data, - height = 300, - loading = false -}) => { - if (loading) { - return ( -
-
-
- ); - } - - const chartData = data.map(item => ({ - ...item, - date: formatDate(item.date), - drawdown: item.drawdown * 100 // Convert to percentage - })); - - const CustomTooltip = ({ active, payload, label }: any) => { - if (active && payload && payload.length) { - return ( -
-

{label}

-

- Drawdown: {formatNumber(payload[0].value, 'percentage')} -

-
- ); - } - return null; - }; - - return ( -
-

Drawdown Chart

- - - - - formatNumber(value, 'percentage')} - /> - } /> - - - -
- ); -}; - -export default DrawdownChart; \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/src/components/MetricCard.tsx b/src/quant_research_starter/frontend/metrics/src/components/MetricCard.tsx deleted file mode 100644 index 82ef0bca..00000000 --- a/src/quant_research_starter/frontend/metrics/src/components/MetricCard.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { MetricCardProps } from '../types'; -import { formatNumber, formatChange, getColorForValue } from '../utils/formatters'; -import { TrendingUp, TrendingDown, HelpCircle } from 'lucide-react'; - -const MetricCard: React.FC = ({ - title, - value, - change, - format = 'number', - description -}) => { - const formattedValue = typeof value === 'number' ? formatNumber(value, format) : value; - const changeColor = change && change >= 0 ? 'text-green-600' : 'text-red-600'; - const valueColor = getColorForValue(typeof value === 'number' ? value : 0, title.toLowerCase()); - - return ( -
-
-

- {title} - {description && ( - - )} -

- {change !== undefined && ( -
- {change >= 0 ? ( - - ) : ( - - )} - {formatChange(change)} -
- )} -
- -
- {formattedValue} -
- - {description && ( -

- {description} -

- )} -
- ); -}; - -export default MetricCard; \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/src/components/PerformanceChart.tsx b/src/quant_research_starter/frontend/metrics/src/components/PerformanceChart.tsx deleted file mode 100644 index 2d532941..00000000 --- a/src/quant_research_starter/frontend/metrics/src/components/PerformanceChart.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - Legend, - ResponsiveContainer -} from 'recharts'; -import { PerformanceDataPoint } from '../types'; -import { formatNumber, formatDate } from '../utils/formatters'; - -interface PerformanceChartProps { - data: PerformanceDataPoint[]; - height?: number; - loading?: boolean; -} - -const PerformanceChart: React.FC = ({ - data, - height = 300, - loading = false -}) => { - if (loading) { - return ( -
-
-
- ); - } - - const chartData = data.map(item => ({ - ...item, - date: formatDate(item.date), - portfolio: item.value, - benchmark: item.benchmark - })); - - const CustomTooltip = ({ active, payload, label }: any) => { - if (active && payload && payload.length) { - return ( -
-

{label}

- {payload.map((entry: any, index: number) => ( -

- {entry.name}: {formatNumber(entry.value, 'currency')} -

- ))} -
- ); - } - return null; - }; - - return ( -
-

Performance Chart

- - - - - formatNumber(value, 'currency')} - /> - } /> - - - - - -
- ); -}; - -export default PerformanceChart; \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/src/hooks/useBacktestData.ts b/src/quant_research_starter/frontend/metrics/src/hooks/useBacktestData.ts deleted file mode 100644 index c301a16a..00000000 --- a/src/quant_research_starter/frontend/metrics/src/hooks/useBacktestData.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { useState, useEffect } from 'react'; -import { BacktestData } from '../types'; - -// Mock data generator - replace with actual API calls -const generateMockData = (): BacktestData => { - const baseValue = 10000; - const performance: any[] = []; - const drawdown: any[] = []; - - let currentValue = baseValue; - let peak = baseValue; - - for (let i = 0; i < 100; i++) { - const date = new Date(); - date.setDate(date.getDate() - (99 - i)); - - // Generate random return between -2% and +3% - const dailyReturn = (Math.random() * 0.05) - 0.02; - currentValue = currentValue * (1 + dailyReturn); - peak = Math.max(peak, currentValue); - const currentDrawdown = (currentValue - peak) / peak; - - performance.push({ - date: date.toISOString().split('T')[0], - value: currentValue, - benchmark: baseValue * (1 + (i * 0.0005)) // Simple benchmark - }); - - drawdown.push({ - date: date.toISOString().split('T')[0], - drawdown: currentDrawdown - }); - } - - const totalReturn = (currentValue - baseValue) / baseValue; - - return { - id: '1', - timestamp: new Date().toISOString(), - metrics: { - totalReturn, - sharpeRatio: 1.2 + Math.random() * 0.8, - maxDrawdown: Math.min(...drawdown.map(d => d.drawdown)), - volatility: 0.15 + Math.random() * 0.1, - winRate: 0.55 + Math.random() * 0.15, - profitFactor: 1.5 + Math.random() * 0.8 - }, - performance, - drawdown - }; -}; - -export const useBacktestData = () => { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Replace this with actual API call - const mockData = generateMockData(); - setData(mockData); - setError(null); - } catch (err) { - setError('Failed to load backtest data'); - console.error('Error fetching backtest data:', err); - } finally { - setLoading(false); - } - }; - - fetchData(); - }, []); - - const refreshData = async () => { - try { - setLoading(true); - await new Promise(resolve => setTimeout(resolve, 800)); - const mockData = generateMockData(); - setData(mockData); - setError(null); - } catch (err) { - setError('Failed to refresh data'); - } finally { - setLoading(false); - } - }; - - return { data, loading, error, refreshData }; -}; \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/src/types/index.ts b/src/quant_research_starter/frontend/metrics/src/types/index.ts deleted file mode 100644 index 5df2dd89..00000000 --- a/src/quant_research_starter/frontend/metrics/src/types/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -export interface BacktestData { - id: string; - timestamp: string; - metrics: { - totalReturn: number; - sharpeRatio: number; - maxDrawdown: number; - volatility: number; - winRate: number; - profitFactor: number; - }; - performance: PerformanceDataPoint[]; - drawdown: DrawdownDataPoint[]; -} - -export interface PerformanceDataPoint { - date: string; - value: number; - benchmark?: number; -} - -export interface DrawdownDataPoint { - date: string; - drawdown: number; -} - -export interface MetricCardProps { - title: string; - value: number | string; - change?: number; - format?: 'percentage' | 'currency' | 'number' | 'ratio'; - description?: string; -} - -export interface ChartProps { - data: PerformanceDataPoint[] | DrawdownDataPoint[]; - height?: number; - loading?: boolean; -} \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/src/utils/formatters.ts b/src/quant_research_starter/frontend/metrics/src/utils/formatters.ts deleted file mode 100644 index c2ef2b45..00000000 --- a/src/quant_research_starter/frontend/metrics/src/utils/formatters.ts +++ /dev/null @@ -1,47 +0,0 @@ -export const formatNumber = (value: number, type: 'percentage' | 'currency' | 'number' | 'ratio' = 'number'): string => { - switch (type) { - case 'percentage': - return `${(value * 100).toFixed(2)}%`; - case 'currency': - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }).format(value); - case 'ratio': - return value.toFixed(3); - default: - return new Intl.NumberFormat('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }).format(value); - } -}; - -export const formatChange = (change: number): string => { - const sign = change >= 0 ? '+' : ''; - return `${sign}${formatNumber(change, 'percentage')}`; -}; - -export const formatDate = (date: string): string => { - return new Date(date).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }); -}; - -export const getColorForValue = (value: number, type: string): string => { - switch (type) { - case 'return': - case 'winRate': - case 'profitFactor': - return value >= 0 ? 'text-green-600' : 'text-red-600'; - case 'drawdown': - case 'volatility': - return value >= 0 ? 'text-red-600' : 'text-green-600'; - default: - return 'text-gray-900'; - } -}; \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/tailwind.config.js b/src/quant_research_starter/frontend/metrics/tailwind.config.js deleted file mode 100644 index 5c2e7b00..00000000 --- a/src/quant_research_starter/frontend/metrics/tailwind.config.js +++ /dev/null @@ -1,20 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: { - colors: { - primary: { - 50: '#eff6ff', - 500: '#3b82f6', - 600: '#2563eb', - 700: '#1d4ed8', - } - } - }, - }, - plugins: [], -} \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/tsconfig.json b/src/quant_research_starter/frontend/metrics/tsconfig.json deleted file mode 100644 index d0104edb..00000000 --- a/src/quant_research_starter/frontend/metrics/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/tsconfig.node.json b/src/quant_research_starter/frontend/metrics/tsconfig.node.json deleted file mode 100644 index 099658cf..00000000 --- a/src/quant_research_starter/frontend/metrics/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} \ No newline at end of file diff --git a/src/quant_research_starter/frontend/metrics/vite.config.ts b/src/quant_research_starter/frontend/metrics/vite.config.ts deleted file mode 100644 index 013977cc..00000000 --- a/src/quant_research_starter/frontend/metrics/vite.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -export default defineConfig({ - plugins: [react()], - server: { - port: 3000, - open: true - }, - build: { - outDir: 'dist', - sourcemap: true - } -}) \ No newline at end of file diff --git a/src/quant_research_starter/frontend/readme.md b/src/quant_research_starter/frontend/readme.md deleted file mode 100644 index 82bd5683..00000000 --- a/src/quant_research_starter/frontend/readme.md +++ /dev/null @@ -1,52 +0,0 @@ -``` -frontend/ -│ -├── core/ # Shared logic library -│ ├── src/ -│ │ ├── components/ # Shared React components -│ │ ├── hooks/ # Shared React hooks (fetch, state mgmt) -│ │ ├── utils/ # Reusable helper functions -│ │ ├── types/ # Shared TypeScript types -│ │ └── index.ts # Export all shared modules -│ ├── package.json -│ ├── tsconfig.json -│ └── README.md - -│ -├── cauweb/ # Main Web UI App -│ ├── src/ -│ │ ├── pages/ # UI Screens (Home, Dashboard, About) -│ │ ├── layouts/ # Shared layouts (Navbar, Sidebar) -│ │ ├── features/ # Domain features (Logs, Trading UI) -│ │ ├── assets/ # Images, icons, fonts -│ │ ├── styles/ # Global CSS/Tailwind configs -│ │ └── main.tsx # App entry point -│ ├── public/ -│ ├── package.json -│ ├── tsconfig.json -│ └── README.md - -│ -├── cli/ # Terminal UI (Node/React Ink CLI) -│ ├── src/ -│ │ └── index.ts -│ ├── package.json -│ └── tsconfig.json - -│ -├── metrics/ # Data visual charts + API metrics -│ ├── src/ -│ │ ├── charts/ -│ │ ├── analytics/ -│ │ └── index.ts -│ ├── package.json -│ └── tsconfig.json - -│ -├── node_modules/ # Shared dependencies root install -│ -├── package.json # root scripts + global deps -├── pnpm-workspace.yaml # defines workspace packages -├── tsconfig.base.json # shared TS config -└── README.md -```