|
|
--- |
|
|
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 |
|
|
|