folio / docs /project-conventions.md
dystomachina's picture
chore: update BEST-PRACTICES
c119a7b
---
description: Concise coding conventions for the Folio project
alwaysApply: true
---
# Folio Project Conventions
This document outlines the key coding conventions for the Folio project. These conventions are designed to help maintain code quality, readability, and consistency across the codebase.
## Project Tech Stack
- **Web Framework**: Dash (Python)
- **Data Processing**: Pandas, NumPy
- **Financial Data**: Yahoo Finance API
- **Testing**: Pytest
- **Linting**: Ruff
## Principles
1. **Follow the Boy Scout Rule**: Leave the code cleaner than you found it.
2. **Don't Repeat Yourself (DRY)**: Extract repeated code into reusable functions.
3. **You Aren't Gonna Need It (YAGNI)**: Don't add functionality until it's necessary.
4. **Optimize After Measuring**: Profile code to identify actual bottlenecks before optimizing.
5. **Use Consistent Formatting**: Use Black, Flake8, and isort to maintain consistent code style.
6. **Imports at Top**: Always place all imports at the top of the file.
7. **No Unused Code**: Remove commented-out code and unused imports/variables.
8. **Configuration Over Hardcoding**: Use configuration files for values that might change.
9. **Log with Context**: Include relevant information in log messages.
10. **Make Small, Focused Changes**: Don't modify unrelated code when implementing a feature or fixing a bug.
## Core Conventions
### 1. Fail Fast and Transparently
Never hide errors with default values. Financial data must be accurate or explicitly marked as unavailable.
```python
# ❌ Bad: Hiding errors with defaults
def get_beta(ticker):
try:
return data_fetcher.get_beta(ticker)
except Exception:
return 1.0 # Dangerous default!
# βœ… Good: Transparent failure
def get_beta(ticker):
try:
return data_fetcher.get_beta(ticker)
except Exception as e:
logger.error(f"Failed to get beta for {ticker}: {e}", exc_info=True)
raise # Let the caller handle the error
```
### 2. Use Intention-Revealing Names
Names should clearly communicate what a variable, function, or class is for.
```python
# ❌ Bad: Unclear names
def calc(p, q):
return p * q * 1.1
# βœ… Good: Clear names
def calculate_total_with_tax(price, quantity):
return price * quantity * 1.1
```
### 3. Write Small, Focused Functions
Each function should do one thing well and be reasonably small.
```python
# ❌ Bad: Function doing too much
def process_portfolio(portfolio_data):
# Validate data
if not portfolio_data:
raise ValueError("Empty portfolio")
# Calculate metrics
total_value = 0
total_beta_adjusted = 0
for position in portfolio_data:
price = position["price"]
quantity = position["quantity"]
beta = get_beta(position["ticker"])
value = price * quantity
total_value += value
total_beta_adjusted += value * beta
# Generate report
report = {
"total_value": total_value,
"portfolio_beta": total_beta_adjusted / total_value if total_value else 0,
"positions": len(portfolio_data)
}
# Save to database
db.save_portfolio_report(report)
return report
# βœ… Good: Functions with single responsibilities
def validate_portfolio(portfolio_data):
if not portfolio_data:
raise ValueError("Empty portfolio")
return portfolio_data
def calculate_position_metrics(position):
price = position["price"]
quantity = position["quantity"]
beta = get_beta(position["ticker"])
value = price * quantity
beta_adjusted = value * beta
return {"value": value, "beta_adjusted": value * beta}
def calculate_portfolio_metrics(portfolio_data):
validated_data = validate_portfolio(portfolio_data)
position_metrics = [calculate_position_metrics(pos) for pos in validated_data]
total_value = sum(pos["value"] for pos in position_metrics)
total_beta_adjusted = sum(pos["beta_adjusted"] for pos in position_metrics)
return {
"total_value": total_value,
"portfolio_beta": total_beta_adjusted / total_value if total_value else 0,
"positions": len(portfolio_data)
}
def save_portfolio_report(report):
db.save_portfolio_report(report)
return report
def process_portfolio(portfolio_data):
metrics = calculate_portfolio_metrics(portfolio_data)
return save_portfolio_report(metrics)
```
### 4. Validate Early, Return Fast
Check inputs at the beginning of functions to avoid deep nesting and keep the happy path clean.
```python
# ❌ Bad: Deeply nested conditionals
def process_data(data):
if data is not None:
if "ticker" in data:
if data["ticker"] != "":
# Process the data...
return result
else:
return None
else:
return None
else:
return None
# βœ… Good: Early validation
def process_data(data):
if data is None:
raise ValueError("Data cannot be None")
if "ticker" not in data:
raise ValueError("Missing required 'ticker' field")
if data["ticker"] == "":
raise ValueError("Ticker cannot be empty")
# Process the data...
return result
```
### 5. Comment the "Why," Not the "What"
Explain reasoning behind complex code, not obvious operations.
```python
# ❌ Bad: Commenting the obvious
# Calculate the sum of prices
total = sum(item.price for item in items)
# ❌ Bad: Commented-out code
# Old calculation method
# for item in items:
# total += item.price
# βœ… Good: Explaining the why
# Apply 15% discount for bulk orders (>10 items) per company policy
if len(items) > 10:
total *= 0.85
```
### 6. Write Minimal, Effective Tests
Focus on testing critical business logic, not framework functionality.
```python
# ❌ Bad: Testing framework functionality
def test_dataframe_creation():
# This just tests pandas functionality, not our code
data = {"ticker": ["AAPL"], "price": [150]}
df = pd.DataFrame(data)
assert len(df) == 1
assert "ticker" in df.columns
# βœ… Good: Testing critical business logic
def test_portfolio_beta_calculation():
# Arrange: Set up test data
portfolio = Portfolio()
portfolio.add_position(
StockPosition(ticker="AAPL", quantity=10, price=150)
)
# Mock external dependencies
data_fetcher = MagicMock()
data_fetcher.get_beta.return_value = 1.2
# Act: Call the method under test
beta = portfolio.calculate_beta(data_fetcher=data_fetcher)
# Assert: Verify the result
assert beta == 1.2
data_fetcher.get_beta.assert_called_once_with("AAPL")
```
### 7. Embrace Pythonic Idioms
Use Python's built-in features to write cleaner, more readable code.
```python
# ❌ Bad: Non-Pythonic code
result = []
for i in range(len(items)):
if items[i].price > 100:
result.append(items[i].name)
# βœ… Good: Pythonic code
result = [item.name for item in items if item.price > 100]
# ❌ Bad: Manual resource management
f = open("data.csv", "r")
try:
data = f.read()
finally:
f.close()
# βœ… Good: Context manager
with open("data.csv", "r") as f:
data = f.read()
```
### 8. Use Type Hints
Add type hints to improve readability and enable static analysis.
```python
# ❌ Bad: No type hints
def calculate_position_value(quantity, price):
return quantity * price
# βœ… Good: With type hints
def calculate_position_value(quantity: float, price: float) -> float:
return quantity * price
# Even better: With more specific types and docstring
from typing import Dict, List, Optional
def get_positions_by_sector(
positions: List[Dict[str, any]],
sector: Optional[str] = None
) -> Dict[str, List[Dict[str, any]]]:
"""
Group positions by sector.
Args:
positions: List of position dictionaries
sector: Optional sector to filter by
Returns:
Dictionary mapping sectors to lists of positions
"""
result = {}
for position in positions:
pos_sector = position.get("sector", "Unknown")
if sector and pos_sector != sector:
continue
if pos_sector not in result:
result[pos_sector] = []
result[pos_sector].append(position)
return result
```
### 9. Handle Errors Gracefully
Use exceptions with context and handle them appropriately.
```python
# ❌ Bad: Using error codes
def divide_stocks(total_value, num_stocks):
if num_stocks == 0:
return -1 # Error code
return total_value / num_stocks
# Usage
result = divide_stocks(1000, 0)
if result == -1:
print("Error: Cannot divide by zero")
# βœ… Good: Using exceptions
def divide_stocks(total_value: float, num_stocks: int) -> float:
if num_stocks == 0:
raise ValueError("Cannot divide by zero stocks")
return total_value / num_stocks
# Usage
try:
result = divide_stocks(1000, 0)
except ValueError as e:
logger.error(f"Portfolio calculation error: {e}")
# Handle the error appropriately
```
### 10. Keep It Simple (KISS)
Prefer simple, straightforward solutions over complex ones.
```python
# ❌ Bad: Overly complex
def is_valid_ticker(ticker):
if ticker is not None:
if isinstance(ticker, str):
if len(ticker) > 0:
if len(ticker) <= 5:
if ticker.isalpha():
return True
return False
# βœ… Good: Simple and clear
def is_valid_ticker(ticker: str) -> bool:
return (
isinstance(ticker, str) and
1 <= len(ticker) <= 5 and
ticker.isalpha()
)
```
### 11. Maintain Separation of Concerns
Business logic MUST reside in the core library (`src/folio/`), not in interface layers (`src/focli/`).
```python
# ❌ Bad: Business logic in CLI layer
# src/focli/utils.py
def calculate_position_value_with_price_change(position_group, price_change):
# Business logic for calculating position value
return new_value
# βœ… Good: Business logic in core library
# src/folio/portfolio_value.py
def calculate_position_value_with_price_change(position_group, price_change):
# Business logic for calculating position value
return new_value
# src/focli/commands/position.py
def handle_position_command(args):
# Only handle user interaction and call core library
result = portfolio_value.calculate_position_value_with_price_change(
position_group, price_change
)
# Format and display result
```
## Benefits of Following These Conventions
- **Readability**: Code is easier to understand at a glance
- **Maintainability**: Simpler structure makes changes easier and safer
- **Testability**: Clear paths make testing more straightforward
- **Reliability**: Proper error handling prevents unexpected behavior
- **Performance**: Well-structured code leads to better performance