Development Guide
This guide covers setting up your development environment, running the system locally, and contributing to the codebase.
Prerequisites
Required
- Python 3.11+ — Download
- uv — Package manager (Installation)
- Doppler CLI — Secrets management (Installation)
- Git — Version control
Optional (for full workflow)
- GitHub CLI (
gh) — For repo operations
Initial Setup
1. Clone Repository
git clone https://github.com/Kartavya-Jharwal/quant-portfolio-monitor.git
cd quant-portfolio-monitor
2. Install Dependencies
This creates .venv/ and installs all 188 dependencies from the lockfile.
3. Configure Doppler
4. Verify Installation
# Check package imports
uv run python -c "import quant_monitor; print('OK')"
# Check config loads
uv run python -c "from quant_monitor.config import cfg; print(cfg.tickers)"
# Setup Appwrite collections (idempotent)
doppler run -- uv run python scripts/setup_appwrite.py
# Run readiness bootstrap
doppler run -- uv run quant-bootstrap
# Run worker
doppler run -- uv run python -m quant_monitor.main
Development Workflow
Running Commands
Always use uv run to execute Python commands within the virtual environment:
# Without secrets
uv run python -m quant_monitor.main
# With Doppler secrets
doppler run -- uv run python -m quant_monitor.main
# Run Rich CLI dashboard
doppler run -- uv run quant-dashboard
Adding Dependencies
# Add a runtime dependency
uv add package-name
# Add a dev dependency
uv add --group dev package-name
# Update lockfile after manual pyproject.toml edits
uv lock
uv sync
Running Tests
# All tests
uv run pytest tests/ -v
# Specific test file
uv run pytest tests/test_config.py -v
# With coverage
uv run pytest tests/ --cov=quant_monitor --cov-report=html
Linting & Formatting
# Check for issues
uv run ruff check .
# Auto-fix issues
uv run ruff check . --fix
# Format code
uv run ruff format .
# Check formatting (CI mode)
uv run ruff format --check .
# Type checking
uv run mypy quant_monitor/ --ignore-missing-imports
Pre-commit Hooks
# Install hooks (one-time)
uv run pre-commit install
# Run manually on all files
uv run pre-commit run --all-files
Project Structure
quant_monitor/
├── __init__.py # Package version
├── main.py # APScheduler entry point
├── config.py # TOML + env loader
├── config.toml # Static configuration
│
├── data/ # Layer 1: Data Pipeline
│ ├── pipeline.py # Orchestrator with failover
│ ├── cache.py # diskcache wrapper
│ ├── appwrite_client.py
│ └── sources/
│ ├── alpaca_feed.py
│ └── fred_feed.py
│
├── features/ # Layer 2: Feature Engineering
│ ├── moving_averages.py
│ ├── volatility.py
│ └── sentiment_features.py
│
├── models/ # Layer 3: Analysis Models
│ ├── technical.py
│ ├── fundamental.py
│ ├── sentiment.py
│ └── macro.py
│
├── agent/ # Layers 4-5: Fusion + Orchestrator
│ ├── fusion.py # Signal fusion engine
│ ├── optimizer.py # Black-Litterman + MVO
│ ├── risk_manager.py # Position limits, kill switch
│ └── alerts.py # Telegram dispatcher
│
├── backtest/ # Validation
│ ├── engine.py # Walk-forward backtester
│ └── metrics.py # Sharpe, Calmar, etc.
│
├── dashboard/ # UI
│ ├── app.py # Rich CLI app
│ ├── data_loader.py # dashboard data adapters
│ └── openbb_views.py # optional OpenBB enrichment
│
└── spiders/ # Scrapy (deploys to Zyte)
├── items.py
├── pipelines.py
├── sec_edgar_spider.py
├── google_rss_spider.py
└── yfinance_spider.py
Configuration
config.toml
All non-secret, version-controlled configuration:
[project]
name = "Ganet - Adaptive Efficiency" # Ganet parent · Team BWC case study
initial_capital = 1_000_000
benchmark = "SPY"
[holdings.SPY]
name = "SPDR S&P 500 ETF"
qty = 295
price_paid = 684.89
sector = "Broad Market"
[cache_ttl]
price_realtime = 60
news = 1800
[regime_weights.CRISIS]
technical = 0.10
fundamental = 0.10
sentiment = 0.20
macro = 0.60
Doppler Secrets
Secrets injected via environment variables:
| Variable | Description |
|---|---|
ALPACA_API_KEY |
Alpaca Markets API key |
ALPACA_SECRET_KEY |
Alpaca Markets secret |
FRED_API_KEY |
Federal Reserve API key |
TELEGRAM_BOT_TOKEN |
Telegram bot token |
TELEGRAM_CHAT_ID |
Telegram chat/group ID |
APPWRITE_ENDPOINT |
Appwrite Cloud endpoint |
APPWRITE_PROJECT_ID |
Appwrite project ID |
APPWRITE_API_KEY |
Appwrite API key |
ZYTE_API_KEY |
Zyte/Scrapy Cloud API key |
Accessing Config in Code
from quant_monitor.config import cfg
# Static config
tickers = cfg.tickers # ['SPY', 'TSM', ...]
benchmark = cfg.benchmark # 'SPY'
cache_ttl = cfg.cache_ttl['price_realtime'] # 60
# Holdings
spy_qty = cfg.holdings['SPY']['qty'] # 295
# Secrets (from Doppler via env)
api_key = cfg.secrets.ALPACA_API_KEY
Testing
Test Organization
tests/
├── conftest.py # Shared fixtures
├── test_config.py # Config loading tests
├── test_data/
│ └── test_pipeline.py
├── test_features/
│ └── test_moving_averages.py
└── test_models/
└── test_technical.py
Fixtures
# tests/conftest.py
@pytest.fixture
def mock_env():
"""Mock environment variables (simulates Doppler injection)."""
env_vars = {
"ALPACA_API_KEY": "test_key",
# ...
}
with patch.dict("os.environ", env_vars):
yield env_vars
@pytest.fixture
def sample_ohlcv():
"""Sample OHLCV DataFrame for testing."""
return pd.DataFrame({
'open': [...],
'high': [...],
# ...
})
Writing Tests
def test_ema_calculation(sample_ohlcv):
"""EMA should match pandas ewm calculation."""
from quant_monitor.features.moving_averages import ema
result = ema(sample_ohlcv['close'], period=9)
expected = sample_ohlcv['close'].ewm(span=9, adjust=False).mean()
pd.testing.assert_series_equal(result, expected)
Deployment
Local Development
# Start worker (signal loop)
doppler run -- uv run python -m quant_monitor.main
# Start dashboard
doppler run -- uv run quant-dashboard --view health
Scrapy Cloud (Zyte)
Documentation
Building Docs
# Live preview
uv run mkdocs serve -f docs/mkdocs.yml
# Build static site
uv run mkdocs build -f docs/mkdocs.yml
Adding Documentation
- Create Markdown file in
docs/ - Add to
navsection indocs/mkdocs.yml - Commit and push — GitHub Actions deploys to Pages
Troubleshooting
Doppler not found
Import errors after uv sync
Tests fail with missing secrets
Tests should mock secrets, not require Doppler:
Code Style
- Line length: 100 characters
- Quotes: Double quotes for strings
- Imports: Sorted by ruff (isort rules)
- Type hints: Required for all public functions
- Docstrings: Google style
def compute_signal(
prices: pd.DataFrame,
regime: str,
threshold: float = 0.35,
) -> dict[str, float]:
"""Compute trading signal for all tickers.
Args:
prices: OHLCV DataFrame with MultiIndex (ticker, date)
regime: Current volatility regime name
threshold: Minimum signal magnitude to trigger action
Returns:
Dictionary mapping ticker to signal score in [-1, 1]
Raises:
ValueError: If regime is not recognized
"""
Last updated: March 14, 2026