Style Guide
This style guide ensures consistency and readability across the Investing Algorithm Framework codebase.
Python Style Guidelines
Code Formatting
We use Black for code formatting with the following configuration:
# pyproject.toml
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
Import Style
Import Order
- Standard library imports
- Related third-party imports
- Local application/library imports
# Standard library
import logging
import os
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Union
# Third-party
import numpy as np
import pandas as pd
import ccxt
from sqlalchemy import Column, Integer, String
# Local
from investing_algorithm_framework.domain import TradingStrategy
from investing_algorithm_framework.infrastructure import CCXTOrderExecutor
from investing_algorithm_framework.services import PortfolioService
Import Guidelines
# Good: Explicit imports
from investing_algorithm_framework.domain import (
TradingStrategy,
BacktestDateRange,
PortfolioConfiguration
)
# Avoid: Wildcard imports
from investing_algorithm_framework.domain import *
# Good: Relative imports for same package
from .portfolio_service import PortfolioService
from ..domain.models import Order
# Avoid: Long relative imports
from ....some.deep.nested.module import SomeClass
Variable Naming
General Rules
# Good: Descriptive names
portfolio_total_value = 10000.0
moving_average_period = 20
btc_current_price = 45000.0
# Bad: Abbreviated or unclear names
ptv = 10000.0
map = 20
bcp = 45000.0
# Good: Boolean variables
is_backtesting = True
has_open_positions = False
should_rebalance = True
# Bad: Ambiguous boolean names
backtesting = True
positions = False
rebalance = True
Class Naming
# Good: PascalCase for classes
class TradingStrategy:
pass
class MovingAverageCrossoverStrategy(TradingStrategy):
pass
class CCXTDataProvider:
pass
# Bad: Snake_case or unclear names
class trading_strategy:
pass
class Strategy1:
pass
class MAStrat:
pass
Method and Function Naming
class PortfolioManager:
# Good: Verb-based method names
def calculate_total_value(self) -> float:
pass
def get_positions(self) -> List[Position]:
pass
def create_buy_order(self, symbol: str, amount: float) -> Order:
pass
def is_position_open(self, symbol: str) -> bool:
pass
# Bad: Unclear or noun-based names
def total(self) -> float:
pass
def positions(self) -> List[Position]:
pass
def order(self, symbol: str, amount: float) -> Order:
pass
Constants
# Good: UPPERCASE with underscores
DEFAULT_TIMEFRAME = "1d"
MAX_POSITION_SIZE = 0.25
API_RATE_LIMIT = 1200 # requests per minute
SUPPORTED_EXCHANGES = ["binance", "coinbase", "kraken"]
# Bad: Mixed case or unclear names
defaultTimeframe = "1d"
maxPos = 0.25
limit = 1200
exchanges = ["binance", "coinbase", "kraken"]
Type Hints
Basic Type Hints
from typing import Dict, List, Optional, Union
from datetime import datetime
def calculate_moving_average(
prices: List[float],
period: int
) -> float:
return sum(prices[-period:]) / period
def get_portfolio_value(
positions: Dict[str, float],
prices: Dict[str, float]
) -> Optional[float]:
if not positions:
return None
return sum(positions[symbol] * prices.get(symbol, 0)
for symbol in positions)
Complex Type Hints
from typing import Callable, TypeVar, Generic, Protocol
# Type variables
T = TypeVar('T')
StrategyType = TypeVar('StrategyType', bound='TradingStrategy')
# Generic classes
class DataProvider(Generic[T]):
def get_data(self, symbol: str) -> T:
pass
# Protocols for structural typing
class Tradeable(Protocol):
def get_current_price(self) -> float: ...
def create_order(self, side: str, amount: float) -> Order: ...
# Complex function signatures
def backtest_strategy(
strategy: TradingStrategy,
data_provider: DataProvider[pd.DataFrame],
date_range: BacktestDateRange,
callback: Optional[Callable[[BacktestRun], None]] = None
) -> BacktestResults:
pass
Documentation Style
Class Documentation
class MovingAverageStrategy(TradingStrategy):
"""
Trading strategy based on moving average crossover signals.
This strategy generates buy signals when a short-term moving average
crosses above a long-term moving average, and sell signals when the
short-term average crosses below the long-term average.
Attributes:
short_period: Number of periods for short moving average
long_period: Number of periods for long moving average
symbol: Trading symbol to apply strategy to
Example:
>>> strategy = MovingAverageStrategy(
... short_period=20,
... long_period=50,
... symbol="BTC/USDT"
... )
>>> app.add_strategy(strategy)
"""
def __init__(
self,
short_period: int = 20,
long_period: int = 50,
symbol: str = "BTC/USDT"
):
"""
Initialize the moving average strategy.
Args:
short_period: Period for short moving average. Must be less
than long_period. Defaults to 20.
long_period: Period for long moving average. Must be greater
than short_period. Defaults to 50.
symbol: Trading symbol to monitor. Defaults to "BTC/USDT".
Raises:
ValueError: If short_period >= long_period
ValueError: If periods are not positive integers
"""
if short_period >= long_period:
raise ValueError("Short period must be less than long period")
if short_period <= 0 or long_period <= 0:
raise ValueError("Periods must be positive integers")
self.short_period = short_period
self.long_period = long_period
self.symbol = symbol
Function Documentation
def calculate_sharpe_ratio(
returns: List[float],
risk_free_rate: float = 0.02,
periods_per_year: int = 252
) -> float:
"""
Calculate the Sharpe ratio for a series of returns.
The Sharpe ratio measures risk-adjusted return by comparing the excess
return of an investment over a risk-free rate to the volatility of the
investment.
Args:
returns: List of periodic returns (e.g., daily returns)
risk_free_rate: Annual risk-free rate. Defaults to 2% (0.02).
periods_per_year: Number of return periods per year. Defaults to
252 (trading days).
Returns:
The Sharpe ratio as a float. Higher values indicate better
risk-adjusted returns.
Raises:
ValueError: If returns list is empty
ValueError: If standard deviation is zero (no volatility)
Example:
>>> daily_returns = [0.01, -0.02, 0.015, 0.008, -0.005]
>>> sharpe = calculate_sharpe_ratio(daily_returns, 0.025)
>>> print(f"Sharpe Ratio: {sharpe:.2f}")
Sharpe Ratio: 1.25
Note:
This function assumes the returns are already in decimal format
(e.g., 0.01 for 1% return).
"""
if not returns:
raise ValueError("Returns list cannot be empty")
mean_return = sum(returns) / len(returns)
std_return = (sum((r - mean_return) ** 2 for r in returns) / len(returns)) ** 0.5
if std_return == 0:
raise ValueError("Cannot calculate Sharpe ratio with zero volatility")
# Annualize the Sharpe ratio
excess_return = mean_return - (risk_free_rate / periods_per_year)
annualized_excess_return = excess_return * periods_per_year
annualized_volatility = std_return * (periods_per_year ** 0.5)
return annualized_excess_return / annualized_volatility
Error Handling Style
Exception Hierarchy
# Base framework exception
class InvestingAlgorithmFrameworkError(Exception):
"""Base exception for all framework-related errors."""
pass
# Domain-specific exceptions
class TradingError(InvestingAlgorithmFrameworkError):
"""Base exception for trading-related errors."""
pass
class OrderError(TradingError):
"""Exception raised for order-related errors."""
pass
class InsufficientFundsError(OrderError):
"""Exception raised when insufficient funds for order."""
pass
class DataError(InvestingAlgorithmFrameworkError):
"""Exception raised for data-related errors."""
pass
class ConnectionError(DataError):
"""Exception raised for connection-related errors."""
pass
Exception Usage
def create_buy_order(
self,
symbol: str,
amount: float,
price: Optional[float] = None
) -> Order:
"""
Create a buy order for the specified symbol and amount.
Args:
symbol: Trading symbol (e.g., "BTC/USDT")
amount: Amount to buy in base currency
price: Limit price (optional, uses market price if None)
Returns:
Created order object
Raises:
ValueError: If amount is not positive
InsufficientFundsError: If account balance is insufficient
ConnectionError: If exchange connection fails
"""
# Input validation
if amount <= 0:
raise ValueError(f"Order amount must be positive, got {amount}")
if not symbol:
raise ValueError("Symbol cannot be empty")
try:
# Check available balance
balance = self._get_available_balance()
if balance < amount:
raise InsufficientFundsError(
f"Insufficient balance: {balance} < {amount}"
)
# Create order
order = self._create_order_on_exchange(symbol, amount, price)
return order
except ConnectionError as e:
# Re-raise connection errors with context
raise ConnectionError(
f"Failed to create order for {symbol}: {e}"
) from e
except Exception as e:
# Log unexpected errors and re-raise
logger.error(f"Unexpected error creating order: {e}", exc_info=True)
raise TradingError(f"Failed to create buy order: {e}") from e
Logging Style
Logger Setup
import logging
# Good: Module-level logger
logger = logging.getLogger(__name__)
class TradingStrategy:
def __init__(self):
# Good: Class-specific logger
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
def apply_strategy(self, algorithm, market_data):
self.logger.info("Executing trading strategy")
try:
current_price = market_data.get_last_price("BTC/USDT")
self.logger.debug(f"Current BTC price: ${current_price:.2f}")
# Strategy logic here
except Exception as e:
self.logger.error(f"Strategy execution failed: {e}", exc_info=True)
raise
Logging Messages
# Good: Informative log messages
logger.info("Starting portfolio rebalancing", extra={
'portfolio_value': portfolio.total_value,
'target_weights': target_weights,
'current_weights': current_weights
})
logger.warning(
"Position size exceeds risk limit",
extra={
'symbol': symbol,
'position_size': position_size,
'risk_limit': risk_limit,
'portfolio_percentage': position_size / portfolio.total_value
}
)
# Bad: Vague or missing information
logger.info("Rebalancing")
logger.warning("Risk limit exceeded")
# Good: Structured logging for production
logger.info("Order executed", extra={
'event_type': 'order_filled',
'order_id': order.id,
'symbol': order.symbol,
'side': order.side,
'amount': order.amount,
'price': order.price,
'timestamp': datetime.utcnow().isoformat()
})
Testing Style
Test Organization
# tests/test_trading_strategy.py
import pytest
from unittest.mock import Mock, patch
from investing_algorithm_framework import TradingStrategy
class TestTradingStrategy:
"""Test suite for TradingStrategy class."""
@pytest.fixture
def strategy(self):
"""Fixture providing a basic strategy instance."""
return TradingStrategy()
@pytest.fixture
def mock_market_data(self):
"""Fixture providing mock market data."""
mock_data = Mock()
mock_data.get_last_price.return_value = 50000.0
mock_data.get_data.return_value = [
{"open": 49000, "high": 51000, "low": 48000, "close": 50000}
]
return mock_data
def test_strategy_initialization(self, strategy):
"""Test strategy can be initialized with default parameters."""
assert strategy is not None
assert hasattr(strategy, 'apply_strategy')
def test_strategy_with_market_data(self, strategy, mock_market_data):
"""Test strategy execution with mock market data."""
algorithm = Mock()
# Should not raise exception
strategy.apply_strategy(algorithm, mock_market_data)
# Verify market data was accessed
mock_market_data.get_last_price.assert_called()
@pytest.mark.parametrize("price,expected_signal", [
(45000, "buy"),
(55000, "sell"),
(50000, "hold")
])
def test_strategy_signals_at_different_prices(
self,
strategy,
price,
expected_signal
):
"""Test strategy generates correct signals at different price levels."""
# Test implementation here
pass
def test_strategy_handles_missing_data(self, strategy):
"""Test strategy handles case when market data is unavailable."""
algorithm = Mock()
market_data = Mock()
market_data.get_last_price.return_value = None
# Should handle gracefully without crashing
strategy.apply_strategy(algorithm, market_data)
def test_strategy_error_handling(self, strategy):
"""Test strategy handles exceptions in market data access."""
algorithm = Mock()
market_data = Mock()
market_data.get_last_price.side_effect = ConnectionError("Network error")
with pytest.raises(ConnectionError):
strategy.apply_strategy(algorithm, market_data)
Assertion Style
# Good: Clear, specific assertions
def test_portfolio_value_calculation():
portfolio = Portfolio()
portfolio.add_position("BTC", 0.1, 50000)
portfolio.add_position("ETH", 2.0, 3000)
expected_value = (0.1 * 50000) + (2.0 * 3000)
actual_value = portfolio.calculate_total_value()
assert actual_value == expected_value
assert portfolio.get_position_count() == 2
# Good: Using pytest.approx for floating point comparisons
def test_sharpe_ratio_calculation():
returns = [0.01, -0.02, 0.015, 0.008, -0.005]
sharpe = calculate_sharpe_ratio(returns, risk_free_rate=0.02)
assert sharpe == pytest.approx(1.25, abs=0.01)
# Good: Testing exception messages
def test_invalid_order_amount():
portfolio = Portfolio()
with pytest.raises(ValueError, match="Order amount must be positive"):
portfolio.create_buy_order("BTC/USDT", -100)
Configuration Style
Environment Configuration
# config/development.py
class DevelopmentConfig:
"""Development environment configuration."""
DEBUG = True
TESTING = False
# Database
DATABASE_URI = "sqlite:///dev.db"
# Logging
LOG_LEVEL = "DEBUG"
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Trading
PAPER_TRADING = True
INITIAL_BALANCE = 10000.0
# API Keys (should be loaded from environment)
EXCHANGE_API_KEY = os.getenv("DEV_EXCHANGE_API_KEY")
EXCHANGE_API_SECRET = os.getenv("DEV_EXCHANGE_API_SECRET")
# config/production.py
class ProductionConfig:
"""Production environment configuration."""
DEBUG = False
TESTING = False
# Database
DATABASE_URI = os.getenv("DATABASE_URI")
# Logging
LOG_LEVEL = "INFO"
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
# Trading
PAPER_TRADING = False
INITIAL_BALANCE = float(os.getenv("INITIAL_BALANCE", "1000"))
# Security
SECRET_KEY = os.getenv("SECRET_KEY")
EXCHANGE_API_KEY = os.getenv("EXCHANGE_API_KEY")
EXCHANGE_API_SECRET = os.getenv("EXCHANGE_API_SECRET")
YAML Configuration Style
# config.yaml
app:
name: "Trading Bot"
version: "1.0.0"
environment: "production"
database:
uri: "${DATABASE_URI}"
pool_size: 10
max_overflow: 20
exchanges:
binance:
api_key: "${BINANCE_API_KEY}"
api_secret: "${BINANCE_API_SECRET}"
sandbox: false
rate_limit: 1200
portfolio:
initial_balance: 10000.0
trading_symbol: "USDT"
max_position_size: 0.25
risk_per_trade: 0.02
strategies:
- name: "MA Crossover"
class: "MovingAverageStrategy"
parameters:
short_period: 20
long_period: 50
symbols: ["BTC/USDT", "ETH/USDT"]
logging:
level: "INFO"
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers:
- type: "file"
filename: "logs/trading.log"
max_size: "10MB"
backup_count: 5
- type: "console"
stream: "stdout"
File Organization
Directory Structure
investing_algorithm_framework/
├── __init__.py
├── app/ # Application layer
│ ├── __init__.py
│ ├── app.py
│ ├── algorithm/
│ ├── analysis/
│ └── strategy/
├── domain/ # Domain models and business logic
│ ├── __init__.py
│ ├── models/
│ ├── services/
│ └── exceptions/
├── infrastructure/ # External integrations
│ ├── __init__.py
│ ├── exchanges/
│ ├── data_providers/
│ └── databases/
├── cli/ # Command-line interface
│ ├── __init__.py
│ └── commands/
└── utils/ # Utility functions
├── __init__.py
├── datetime_utils.py
└── math_utils.py
tests/
├── __init__.py
├── unit/
├── integration/
├── fixtures/
└── conftest.py
docs/
├── api/