Contributing to openseries

We welcome contributions to openseries! This guide will help you get started with contributing to the project.

Getting Started

Development Setup

  1. Fork the repository on GitHub

  2. Clone your fork locally:

git clone https://github.com/yourusername/openseries.git
cd openseries
  1. Install Poetry (if not already installed):

curl -sSL https://install.python-poetry.org | python3 -
  1. Install dependencies:

poetry install
  1. Activate the virtual environment:

poetry shell
  1. Install pre-commit hooks:

pre-commit install

Development Workflow

  1. Create a new branch for your feature or bug fix:

git checkout -b feature/your-feature-name
  1. Make your changes

  2. Run tests to ensure everything works:

make test
  1. Run linting and type checking:

make lint
  1. Commit your changes:

git add .
git commit -m "Add your descriptive commit message"
  1. Push to your fork:

git push origin feature/your-feature-name
  1. Create a pull request on GitHub

Code Standards

Code Style

openseries uses several tools to maintain code quality:

  • Ruff: For linting and code formatting

  • mypy: For static type checking

  • pre-commit: For automated checks before commits

The configuration for these tools is in pyproject.toml.

Type Hints

All new code should include proper type hints:

def calculate_returns(prices: list[float]) -> list[float]:
     """Calculate simple returns from prices."""
     returns = []
     for i in range(1, len(prices)):
          ret = (prices[i] / prices[i-1]) - 1
          returns.append(ret)
     return returns

Docstrings

Use Google-style docstrings for all public functions and classes:

def calculate_sharpe_ratio(returns: list[float], risk_free_rate: float = 0.0) -> float:
     """Calculate the Sharpe ratio.

     Args:
          returns: List of periodic returns.
          risk_free_rate: Risk-free rate for the same period. Defaults to 0.0.

     Returns:
          The Sharpe ratio.

     Raises:
          ValueError: If returns list is empty.

     Example:
          >>> returns = [0.01, 0.02, -0.01, 0.03]
          >>> sharpe = calculate_sharpe_ratio(returns)
          >>> print(f"Sharpe ratio: {sharpe:.3f}")
     """
     if not returns:
          raise ValueError("Returns list cannot be empty")

     mean_return = sum(returns) / len(returns)
     std_dev = (sum((r - mean_return) ** 2 for r in returns) / len(returns)) ** 0.5

     if std_dev == 0:
          return 0.0

     return (mean_return - risk_free_rate) / std_dev

Testing

Test Structure

Tests are located in the tests/ directory and use pytest:

tests/
├── __init__.py
├── test_series.py
├── test_frame.py
├── test_portfoliotools.py
└── ...

Writing Tests

Write comprehensive tests for new functionality:

import pytest
import pandas as pd
from pandas.testing import assert_frame_equal
from openseries import OpenTimeSeries

class TestOpenTimeSeries:
     """Test cases for OpenTimeSeries class."""

     def test_from_arrays_basic(self):
          """Test basic creation from arrays."""
          dates = ['2023-01-01', '2023-01-02', '2023-01-03']
          values = [100.0, 102.0, 99.0]

          series = OpenTimeSeries.from_arrays(dates=dates, values=values, name="Test")

          if series.label != "Test":
                msg = f"Expected name 'Test', got '{series.label}'"
                raise ValueError(msg)
          if series.length != 3:
                msg = f"Expected length 3, got {series.length}"
                raise ValueError(msg)
          if series.first_idx != pd.Timestamp('2023-01-01').date():
                msg = f"Expected first_idx 2023-01-01, got {series.first_idx}"
                raise ValueError(msg)
          if series.last_idx != pd.Timestamp('2023-01-03').date():
                msg = f"Expected last_idx 2023-01-03, got {series.last_idx}"
                raise ValueError(msg)

     def test_from_arrays_invalid_dates(self):
          """Test that invalid dates raise appropriate errors."""
          with pytest.raises(ValueError):
                OpenTimeSeries.from_arrays(
                     dates=['invalid-date'],
                     values=[100.0],
                     name="Test"
                )

     def test_calculate_returns(self):
          """Test return calculation."""
          dates = ['2023-01-01', '2023-01-02', '2023-01-03']
          values = [100.0, 102.0, 99.0]

          series = OpenTimeSeries.from_arrays(dates=dates, values=values, name="Test")
          series.value_to_ret()  # Modifies original

          expected_returns = [0.02, -0.0294117647]  # Approximate
          actual_returns = series.values

          if len(actual_returns) != 2:
                msg = f"Expected 2 returns, got {len(actual_returns)}"
                raise ValueError(msg)
          # Use tolerance-based comparison
          if abs(actual_returns[0] - expected_returns[0]) >= 1e-6:
                msg = f"First return mismatch: {actual_returns[0]} vs {expected_returns[0]}"
                raise ValueError(msg)
          if abs(actual_returns[1] - expected_returns[1]) >= 1e-6:
                msg = f"Second return mismatch: {actual_returns[1]} vs {expected_returns[1]}"
                raise ValueError(msg)

Running Tests

Run all tests:

make test

Run specific test files:

pytest tests/test_series.py

Run tests with coverage:

pytest --cov=openseries tests/

Test Coverage

openseries maintains high test coverage (>99%). New code should include comprehensive tests:

  • Test normal use cases

  • Test edge cases

  • Test error conditions

  • Test with different data types and sizes

Documentation

Documentation Standards

  • All public APIs must be documented

  • Include examples in docstrings where helpful

  • Update relevant documentation files when adding features

  • Use clear, concise language

Building Documentation

To build documentation locally:

cd docs
make html

The built documentation will be in docs/_build/html/.

Contributing Guidelines

Pull Request Process

  1. Fork and Branch: Create a feature branch from master

  2. Develop: Make your changes with tests and documentation

  3. Test: Ensure all tests pass and coverage remains high

  4. Lint: Run linting and fix any issues

  5. Document: Update documentation as needed

  6. Commit: Use clear, descriptive commit messages

  7. Pull Request: Create a PR with a clear description

Commit Messages

Use clear, descriptive commit messages:

Add support for custom business day calendars

- Implement custom calendar functionality in datefixer module
- Add tests for various calendar configurations
- Update documentation with examples
- Fixes #123

Code Review Process

All contributions go through code review:

  1. Automated checks must pass (tests, linting, type checking)

  2. At least one maintainer review is required

  3. Address any feedback or requested changes

  4. Once approved, the PR will be merged

Types of Contributions

Bug Reports

When reporting bugs, please include:

  • Clear description of the issue

  • Steps to reproduce

  • Expected vs. actual behavior

  • Environment details (Python version, OS, etc.)

  • Minimal code example if possible

Feature Requests

For new features:

  • Describe the use case and motivation

  • Provide examples of how it would be used

  • Consider backward compatibility

  • Discuss implementation approach if you have ideas

Code Contributions

Areas where contributions are especially welcome:

  • New financial metrics: Additional risk measures, performance ratios

  • Data sources: Integration with new data providers

  • Visualization: Enhanced plotting capabilities

  • Performance: Optimization of calculations

  • Documentation: Examples, tutorials, API documentation

Documentation Contributions

Documentation improvements are always welcome:

  • Fix typos or unclear explanations

  • Add examples to existing documentation

  • Create new tutorials or guides

  • Improve API documentation

Development Environment

IDE Setup

For VS Code, recommended extensions:

  • Python

  • Pylance

  • Ruff

  • mypy

Recommended settings in .vscode/settings.json:

{
    "python.defaultInterpreterPath": ".venv/bin/python",
    "python.linting.enabled": true,
    "python.linting.ruffEnabled": true,
    "python.formatting.provider": "ruff",
    "python.typeChecking": "strict"
}

Debugging

For debugging tests:

pytest --pdb tests/test_specific.py::test_function

For debugging with VS Code, create .vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Current File",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal"
        },
        {
            "name": "Python: Pytest",
            "type": "python",
            "request": "launch",
            "module": "pytest",
            "args": ["${workspaceFolder}/tests"],
            "console": "integratedTerminal"
        }
    ]
}

Release Process

openseries follows semantic versioning (MAJOR.MINOR.PATCH):

  • MAJOR: Breaking changes

  • MINOR: New features, backward compatible

  • PATCH: Bug fixes, backward compatible

Releases are managed by maintainers and include:

  1. Version bump in pyproject.toml

  2. Update CHANGELOG.md

  3. Create GitHub release with release notes

  4. Publish to PyPI and conda-forge

Getting Help

If you need help with contributing:

  • Check existing issues and discussions on GitHub

  • Ask questions in GitHub Discussions

  • Reach out to maintainers

Community Guidelines

openseries is committed to providing a welcoming and inclusive environment:

  • Be respectful and constructive in all interactions

  • Focus on what is best for the community

  • Show empathy towards other community members

  • Welcome newcomers and help them get started

Thank you for contributing to openseries!