trading/.planning/codebase/CONVENTIONS.md
2026-02-23 20:04:05 +00:00

276 lines
11 KiB
Markdown

# Coding Conventions
**Analysis Date:** 2026-02-23
## Naming Patterns
**Files (Python):**
- Modules: lowercase with underscores (e.g., `redis_streams.py`, `finbert_analyzer.py`)
- Packages: underscores, not hyphens (e.g., `news_fetcher`, `sentiment_analyzer`)
- Private modules: prefix with underscore (e.g., `_deduplicate_and_publish`)
**Files (TypeScript/React):**
- Components: PascalCase (e.g., `MetricsRow.tsx`, `ProtectedRoute.tsx`)
- Hooks: camelCase with `use` prefix (e.g., `useAuth.ts`, `useWebSocket.ts`)
- Utilities/APIs: camelCase (e.g., `client.ts`, `auth.ts`)
- Config files: camelCase with dots (e.g., `tsconfig.app.json`, `vite.config.ts`)
**Functions and Methods:**
- Snake_case in Python (e.g., `_deduplicate_and_publish`, `ensure_group`)
- CamelCase in TypeScript/React (e.g., `startRegistration`, `useCallback`)
- Async functions: prefix with `async` keyword, no special naming convention
- Private/internal functions: prefix with underscore in Python (e.g., `_make_config`, `_make_signal`)
**Variables:**
- Snake_case in Python for all variables (e.g., `session_factory`, `confidence_threshold`)
- CamelCase in TypeScript (e.g., `isAuthenticated`, `loading`, `token`)
- Constants: UPPER_CASE with underscores (e.g., `SEEN_HASHES_KEY = "news:seen_hashes"`, `RAW_STREAM = "test:news:raw"`)
- Loop variables and temporaries: follow standard conventions (e.g., `msg_id`, `score`, `ticker`)
**Types and Classes:**
- Classes: PascalCase (e.g., `StreamPublisher`, `TradeEvaluator`, `BaseStrategy`)
- Enums: PascalCase (e.g., `TradeSide`, `TradeStatus`, `SignalDirection`)
- Type hints: use full import path or import explicitly (e.g., `list[PositionInfo]`, `dict[str, str]`)
- Generic types: use Python 3.12+ syntax without `typing.` prefix (e.g., `dict`, `list`, `tuple`)
**Interfaces (TypeScript):**
- PascalCase with `Props` suffix for component props (e.g., `MetricsRowProps`)
- PascalCase for return types (e.g., `UseAuthReturn`)
- Prefix common interfaces: `I` is not used; use descriptive names instead
## Code Style
**Formatting (Python):**
- Line length: 120 characters (configured in `pyproject.toml` under `[tool.ruff]`)
- Target Python version: 3.12 (configured under `[tool.ruff] target-version`)
- Formatter: Ruff (for linting and formatting)
- No Black configuration — Ruff is the primary tool
**Formatting (TypeScript/React):**
- Target: ES2022 (configured in `tsconfig.app.json`)
- Module resolution: bundler
- Strict mode: enabled (`"strict": true`)
- JSX mode: react-jsx
**Linting (Python):**
- Tool: Ruff (`ruff>=0.3`)
- Key rules: Follow Python 3.12 conventions, enforce async functions
- MyPy: enabled (`mypy>=1.8`)
- `warn_return_any = true`
- `warn_unused_configs = true`
**Linting (TypeScript):**
- ESLint with flat config (`eslint.config.js` in `dashboard/`)
- Extends:
- `@eslint/js` (recommended)
- `typescript-eslint` (recommended)
- `react-hooks` (react-hooks/exhaustive-deps)
- `react-refresh` (for Vite)
- Global ignores: `dist/`
- Files: `**/*.{ts,tsx}`
## Import Organization
**Python Import Order:**
1. `from __future__ import annotations` (always first if present)
2. Standard library: `import asyncio`, `import json`, `import logging`, etc.
3. Third-party packages: `from redis.asyncio import Redis`, `from pydantic import BaseModel`, etc.
4. Shared modules: `from shared.config import BaseConfig`, `from shared.redis_streams import StreamPublisher`
5. Service-specific modules: `from services.news_fetcher.config import NewsFetcherConfig`
6. Absolute imports preferred; relative imports acceptable within a service
**TypeScript Import Order:**
1. React and core libraries: `import { useState, useCallback } from 'react'`
2. Routing: `import { Routes, Route } from 'react-router-dom'`
3. Third-party UI/packages: `import { startRegistration } from '@simplewebauthn/browser'`
4. Internal utilities/API: `import { client } from '../api/client'`
5. Components: `import { MetricsRow } from '../components/MetricsRow'`
6. Hooks: `import { useAuth } from '../hooks/useAuth'`
**Path Aliases:**
- Python: No path aliases configured; use absolute imports from project root
- TypeScript: None configured in `tsconfig.app.json`; use relative paths with `../`
## Error Handling
**Python Error Handling:**
- Use `try/except` for external service calls (Redis, database, API calls)
- Log all exceptions at the point of handling: `logger.exception("Context: %s", variable)`
- Specific exception handling before generic: `except IntegrityError:` before `except Exception:`
- Re-raise specific errors or return default/None on fallback paths
- Example from `sentiment_analyzer/main.py`:
```python
try:
async with db_session_factory() as session:
# persist to DB
except IntegrityError:
# Handle duplicate
pass
except Exception:
logger.exception("Error processing article: %s", data.get("title", "<unknown>"))
```
- Async functions use `try/except` around `await` calls
- Use `asyncio.TimeoutError` for timeout handling
**TypeScript/React Error Handling:**
- Catch errors from API calls and set error state: `err?.response?.data?.detail || err?.message`
- Always provide fallback messages: `'Registration failed'` as default
- Log error context: `throw err` after setting error state to propagate to caller if needed
- Example from `useAuth.ts`:
```typescript
catch (err: any) {
const message = err?.response?.data?.detail || err?.message || 'Registration failed';
setError(message);
throw err;
}
```
## Logging
**Framework:** Python uses `logging` module; no structured logging library
**Patterns:**
- Create logger at module level: `logger = logging.getLogger(__name__)`
- Use appropriate levels:
- `logger.debug()` — Detailed debug info (e.g., "Published to stream:xxx")
- `logger.info()` — General informational (e.g., "Created consumer group…")
- `logger.warning()` — Warning conditions (e.g., "Order rejected by broker")
- `logger.exception()` — Log exceptions with stack trace (always use at exception point)
- Always include context in log messages using `%` formatting: `logger.info("Message with %s", variable)`
- Do NOT use f-strings in logging (allows lazy evaluation)
- Log level configuration: `log_level` in `BaseConfig` (default: "INFO")
**Examples from codebase:**
```python
logger.debug("Published to %s: %s", self.stream, msg_id)
logger.info("Created consumer group %s on %s", self.group, self.stream)
logger.exception("Ollama analysis failed") # Includes stack trace
logger.warning("Order rejected by Alpaca: %s", exc)
```
## Comments
**When to Comment:**
- Complex business logic that isn't self-documenting (e.g., weight adjustment formulas, P&L calculations)
- Non-obvious design decisions (e.g., "SADD returns 1 if the member was added (i.e. not already present)")
- Caveats or workarounds (e.g., "BUSYGROUP means group already exists — expected on subsequent starts.")
- Public APIs and exported functions should have docstrings
**When NOT to Comment:**
- Self-explanatory code (variable names, clear function logic)
- Redundant comments that repeat the code
- Outdated comments (remove or update)
**Docstring/Comments:**
- Module docstrings: Present at top of file, describe purpose
```python
"""Thin wrappers around redis-py Streams for publish/consume with JSON serialization."""
```
- Class docstrings: Describe the class purpose and key behavior
```python
class StreamPublisher:
"""Publishes JSON-encoded messages to a Redis Stream."""
```
- Method docstrings: Use Google/NumPy style for async methods
```python
async def ensure_group(self) -> None:
"""Create the consumer group if it does not already exist."""
```
- Parameters/Returns documented in docstrings for public APIs:
```python
"""Score a single article and publish one ScoredArticle per extracted ticker.
Parameters
----------
article:
The raw article consumed from the ``news:raw`` stream.
"""
```
**Inline Comments:**
- Prefix with `#` and space: `# This explains why…`
- Place above the code line (not at end of line unless very brief)
- Example: `# SADD returns 1 if the member was added (i.e. not already present)`
## Function Design
**Size Guidelines:**
- Functions should be focused and single-responsibility
- Async functions in services are typically 5-30 lines (after docstring)
- Helper functions (prefixed with `_`) are 3-20 lines
- Example: `_deduplicate_and_publish` is ~20 lines, handles one concern
**Parameters:**
- Use keyword arguments for clarity in calls: `await publish(data=article, index=stream)`
- Type hints are required for all parameters and returns
- Default parameters allowed (e.g., `batch_size: int = 10`)
- For configuration, pass config objects rather than many flags: `config: MyConfig` not `threshold=0.5, timeout=10`
**Return Values:**
- Use union types for optional/multiple returns: `TradeSignal | None` (Python 3.10+ syntax)
- Return early for error cases (guard clauses):
```python
if not tickers:
logger.debug("No tickers found")
return
```
- For async functions, return types should use `Awaitable` or direct annotation: `async def x() -> PositionInfo:`
## Module Design
**Exports:**
- Use `__all__` in modules that re-export from submodules
- Example from `shared/models/__init__.py`:
```python
__all__ = [
"Base",
"TimestampMixin",
# Trading
"Strategy",
"Signal",
# ...
]
```
- Imports are grouped by category (Trading, News, Learning, Auth, Timeseries) with comments
**Barrel Files:**
- `shared/models/__init__.py` acts as a barrel, importing all models so Alembic can discover them
- Comment each import group for clarity
- Do NOT use wildcard imports (`from x import *`)
**Config Classes:**
- All config classes extend `shared.config.BaseConfig` (Pydantic BaseSettings)
- Use `model_config = {"env_prefix": "TRADING_"}` for environment variable loading
- Example: `services/news_fetcher/config.py` extends `BaseConfig`
**Service Entry Points:**
- Located at `services/{service_name}/main.py`
- Contains `async def main()` or async generator functions
- Modules imported from shared libraries and service-specific modules
- Signal handling for graceful shutdown
## Special Conventions
**Redis Stream Constants:**
- Define at module level as uppercase strings
- Example: `NEWS_RAW_STREAM = "news:raw"`, `SEEN_HASHES_KEY = "news:seen_hashes"`
**Pydantic Models:**
- Use `model_dump(mode="json")` when serializing to JSON (v2 syntax)
- Set `model_config = {"from_attributes": True}` for ORM mapping
- Enum fields use value comparison: `TradeSide.BUY == "BUY"`
**SQLAlchemy (2.0+):**
- Use async sessions: `async_sessionmaker` with async context managers
- Use `mapped_column` style (not `Column`)
- Type hints required on model fields
- Example: models in `shared/models/trading.py`
**Test Naming:**
- Test functions: `test_{component}_{scenario}` (e.g., `test_publisher_publishes_json`)
- Test classes: `Test{ComponentName}` (e.g., `TestEvaluateProfitableTrade`)
- Helper functions in tests: prefix with `_make_` (e.g., `_make_config`, `_make_signal`)
---
*Convention analysis: 2026-02-23*