diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md index 48d5f81..b894315 100644 --- a/.github/ISSUE_TEMPLATE/custom.md +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -6,5 +6,3 @@ labels: '' assignees: '' --- - - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..272f795 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v2 + + - name: Set up Python + run: uv python install 3.10 + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: Run ruff format check + run: uv run ruff format --check src + + - name: Run ruff linter + run: uv run ruff check src + + - name: Run mypy + run: uv run mypy src/ + + test: + runs-on: ubuntu-latest + strategy: + max-parallel: 1 # Run tests sequentially to avoid rate limiting + matrix: + python-version: ['3.10', '3.11', '3.12'] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v2 + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: Add delay to avoid rate limiting + run: | + echo "Waiting 30 seconds to avoid API rate limiting..." + sleep 30 + + - name: Run tests with coverage + env: + ALPACA_API_KEY: ${{ secrets.ALPACA_API_KEY }} + ALPACA_SECRET_KEY: ${{ secrets.ALPACA_SECRET_KEY }} + run: | + uv run pytest --cov=py_alpaca_api --cov-report=xml --cov-report=term-missing tests + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + fail_ci_if_error: false + verbose: true diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index 930f3fd..5c1d8f4 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -4,7 +4,7 @@ on: push: branches: [main] pull_request: - + env: POETRY_VERSION: 1.8.3 ALPACA_API_KEY: ${{ secrets.ALPACA_API_KEY }} @@ -13,7 +13,7 @@ jobs: build: runs-on: ${{ matrix.os }} strategy: - max-parallel: 1 + max-parallel: 1 # Run sequentially to avoid rate limiting fail-fast: false matrix: python-version: ["3.12"] @@ -40,7 +40,7 @@ jobs: # - uses: actions/setup-python@v5 # with: # python-version: ${{ matrix.python-version }} - + # # Cache the installation of Poetry itself, e.g. the next step. This prevents the workflow # # from installing Poetry every time, which can be slow. Note the use of the Poetry version # # number in the cache key, and the "-0" suffix: this allows you to invalidate the cache @@ -50,7 +50,7 @@ jobs: # with: # path: ~/.local # key: poetry-cache-${{ runner.os }}-${{ matrix.python-version }}-${{ env.POETRY_VERSION }} - + # # Install Poetry. You could do this manually, or there are several actions that do this. # # `snok/install-poetry` seems to be minimal yet complete, and really just calls out to # # Poetry's default install script, which feels correct. I pin the Poetry version here @@ -65,7 +65,7 @@ jobs: # version: 1.8.3 # virtualenvs-create: true # virtualenvs-in-project: true - + # # Cache your dependencies (i.e. all the stuff in your `pyproject.toml`) # - name: cache venv # uses: actions/cache@v4 @@ -76,4 +76,4 @@ jobs: # if: steps.cache-deps.outputs.cache-hit != 'true' # - run: poetry install --no-interaction # - run: poetry run ruff check --fix - # - run: poetry run pytest \ No newline at end of file + # - run: poetry run pytest diff --git a/.gitignore b/.gitignore index 71c95dd..6b83b65 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,21 @@ __pycache__/ *.py[cod] *$py.class - +.ruff_cache/ +.mypy_cache/ +.pytest_cache/ +.coverage +.coverage.* +.coverage.xml +.coverage.html +.coverage.json +.coverage.yaml +.coverage.yml +.coverage.xml +.coverage.html +.coverage.json +.coverage.yaml +.coverage.yml # C extensions *.so .idea/ @@ -164,8 +178,20 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -<<<<<<< HEAD -test.sh -======= -# test.sh ->>>>>>> e0e18b93a48f51c1ed656a62155be25a128e460c +# test.sh - keep this file for local testing + +# Ruff +.ruff_cache/ + +# UV +.venv/ + +# MacOS +.DS_Store + +# Editor directories and files +.idea/ +.vscode/ +*.swp +*.swo +*~ diff --git a/.grok/settings.json b/.grok/settings.json deleted file mode 100644 index 1dbc7db..0000000 --- a/.grok/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "model": "grok-4-latest" -} \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c58ca8..a13137c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,53 @@ +# Pre-commit hooks for code quality and consistency +# See https://pre-commit.com for more information + repos: -- repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.6.8 - hooks: - # Run the linter. - - id: ruff - args: [ --fix ] - # Run the formatter. - - id: ruff-format \ No newline at end of file + # Pre-commit framework hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + args: ['--maxkb=1000'] + - id: check-json + - id: check-toml + - id: check-merge-conflict + - id: check-case-conflict + - id: detect-private-key + - id: debug-statements + - id: mixed-line-ending + args: ['--fix=lf'] + + # Ruff - Fast Python linter and formatter + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.8 + hooks: + # Run the linter + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + # Run the formatter + - id: ruff-format + + # MyPy - Static type checker + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.2 + hooks: + - id: mypy + additional_dependencies: + - types-requests + - types-beautifulsoup4 + - pandas-stubs + args: [--config-file=pyproject.toml] + pass_filenames: true + exclude: ^tests/ + +# Configuration +default_language_version: + python: python3.10 + +ci: + autofix_prs: true + autoupdate_schedule: weekly + autoupdate_commit_msg: 'chore: update pre-commit hooks' diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c2a8fda..ba61fbb 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,4 +9,4 @@ sphinx: python: install: - - requirements: docs/rtd_requirements.txt \ No newline at end of file + - requirements: docs/rtd_requirements.txt diff --git a/CLAUDE.md b/CLAUDE.md index b220408..20cb8f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,113 +1,423 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides comprehensive guidance to Claude Code (claude.ai/code) and other AI assistants when working with the py-alpaca-api codebase. -## Development Commands +## ๐ŸŽฏ Project Overview -### Package Management -This project uses `uv` for dependency management (migrated from Poetry). +**py-alpaca-api** is a modern Python wrapper for the Alpaca Trading API that provides: +- Complete trading operations (orders, positions, account management) +- Market data access (historical, real-time quotes, news) +- Stock analysis tools (screeners, ML predictions, sentiment) +- Full type safety with mypy strict mode +- Comprehensive test coverage (109+ tests) + +**Current Version**: 2.2.0 +**Python Support**: 3.10+ +**License**: MIT + +## ๐Ÿ› ๏ธ Development Setup +### Prerequisites +- Python 3.10 or higher +- uv package manager (recommended) or pip +- Alpaca API credentials (paper trading credentials for testing) + +### Initial Setup ```bash -# Install dependencies +# Clone and enter the repository +git clone https://github.com/TexasCoding/py-alpaca-api.git +cd py-alpaca-api + +# Install dependencies with uv (recommended) uv sync --all-extras --dev -# Add a new dependency +# Or with pip +pip install -e ".[dev]" + +# Install pre-commit hooks +pre-commit install + +# Set up environment variables (create .env file) +echo "ALPACA_API_KEY=your_api_key" >> .env +echo "ALPACA_SECRET_KEY=your_secret_key" >> .env +``` + +## ๐Ÿ“ Development Commands + +### Package Management +```bash +# Install all dependencies +uv sync --all-extras --dev + +# Add a runtime dependency uv add -# Add a dev dependency +# Add a development dependency uv add --dev + +# Update dependencies +uv lock --upgrade + +# Show dependency tree +uv tree ``` ### Testing ```bash -# Run all tests +# Run all tests with API credentials +./test.sh + +# Run specific test file +./test.sh tests/test_trading/test_orders.py + +# Run tests with pytest directly uv run pytest tests +# Run with coverage report +uv run pytest --cov=py_alpaca_api --cov-report=html + # Run tests quietly uv run pytest -q tests -# Run specific test file -uv run pytest tests/test_trading/test_orders.py - -# Run with test script (includes API keys) -./test.sh - -# Run specific tests with test script -./test.sh tests/test_trading/test_orders.py +# Run tests with markers +uv run pytest -m "not slow" ``` ### Code Quality ```bash -# Run linter -uv run ruff check +# Run all quality checks (recommended before committing) +make check + +# Format code +make format +uv run ruff format src tests -# Run linter with auto-fix +# Lint code +make lint uv run ruff check --fix -# Format code -uv run ruff format +# Type checking +make type-check +uv run mypy src + +# Run pre-commit hooks manually +pre-commit run --all-files +``` + +### Development Workflow +```bash +# Common development workflow +make format # Format code +make check # Run all checks +./test.sh # Run tests +git add . # Stage changes +git commit # Commit (triggers pre-commit hooks) ``` -## Architecture Overview +## ๐Ÿ—๏ธ Architecture -### Core API Structure -The repository implements a Python wrapper for the Alpaca Trading API with the following architecture: +### Project Structure +``` +py-alpaca-api/ +โ”œโ”€โ”€ src/py_alpaca_api/ +โ”‚ โ”œโ”€โ”€ __init__.py # Main PyAlpacaAPI class +โ”‚ โ”œโ”€โ”€ exceptions.py # Custom exception hierarchy +โ”‚ โ”œโ”€โ”€ trading/ # Trading operations +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py # Trading module exports +โ”‚ โ”‚ โ”œโ”€โ”€ account.py # Account management +โ”‚ โ”‚ โ”œโ”€โ”€ orders.py # Order execution & management +โ”‚ โ”‚ โ”œโ”€โ”€ positions.py # Position tracking +โ”‚ โ”‚ โ”œโ”€โ”€ watchlists.py # Watchlist CRUD +โ”‚ โ”‚ โ”œโ”€โ”€ market.py # Market hours & calendar +โ”‚ โ”‚ โ”œโ”€โ”€ news.py # Financial news aggregation +โ”‚ โ”‚ โ””โ”€โ”€ recommendations.py # Stock sentiment analysis +โ”‚ โ”œโ”€โ”€ stock/ # Market data & analysis +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py # Stock module exports +โ”‚ โ”‚ โ”œโ”€โ”€ assets.py # Asset information +โ”‚ โ”‚ โ”œโ”€โ”€ history.py # Historical data retrieval +โ”‚ โ”‚ โ”œโ”€โ”€ screener.py # Gainers/losers screening +โ”‚ โ”‚ โ”œโ”€โ”€ predictor.py # ML predictions (Prophet) +โ”‚ โ”‚ โ””โ”€โ”€ latest_quote.py # Real-time quotes +โ”‚ โ”œโ”€โ”€ models/ # Data models +โ”‚ โ”‚ โ”œโ”€โ”€ account_model.py # Account dataclass +โ”‚ โ”‚ โ”œโ”€โ”€ order_model.py # Order dataclass +โ”‚ โ”‚ โ”œโ”€โ”€ position_model.py # Position dataclass +โ”‚ โ”‚ โ”œโ”€โ”€ asset_model.py # Asset dataclass +โ”‚ โ”‚ โ”œโ”€โ”€ watchlist_model.py # Watchlist dataclass +โ”‚ โ”‚ โ”œโ”€โ”€ quote_model.py # Quote dataclass +โ”‚ โ”‚ โ”œโ”€โ”€ clock_model.py # Market clock dataclass +โ”‚ โ”‚ โ””โ”€โ”€ model_utils.py # Conversion utilities +โ”‚ โ””โ”€โ”€ http/ # HTTP layer +โ”‚ โ””โ”€โ”€ requests.py # Request handling with retries +โ”œโ”€โ”€ tests/ # Test suite +โ”‚ โ”œโ”€โ”€ test_trading/ # Trading tests +โ”‚ โ”œโ”€โ”€ test_stock/ # Stock tests +โ”‚ โ”œโ”€โ”€ test_models/ # Model tests +โ”‚ โ””โ”€โ”€ test_http/ # HTTP tests +โ”œโ”€โ”€ docs/ # Documentation +โ”œโ”€โ”€ .github/ # GitHub Actions CI/CD +โ”œโ”€โ”€ pyproject.toml # Project configuration +โ”œโ”€โ”€ Makefile # Development tasks +โ”œโ”€โ”€ test.sh # Test runner script +โ””โ”€โ”€ README.md # User documentation +``` + +### Key Design Patterns -1. **Main Entry Point**: `PyAlpacaAPI` class in `src/py_alpaca_api/__init__.py` - - Initializes with API key, secret, and paper trading flag - - Provides access to `trading` and `stock` modules +1. **Factory Pattern**: All models use `from_dict()` methods for instantiation + ```python + order = order_class_from_dict(api_response_dict) + ``` -2. **Trading Module** (`src/py_alpaca_api/trading/`) - - `account.py`: Account information, activities, and portfolio history - - `orders.py`: Order management and execution - - `positions.py`: Position tracking and management - - `watchlists.py`: Watchlist CRUD operations - - `market.py`: Market clock and calendar data - - `news.py`: Financial news from Yahoo Finance and Benzinga - - `recommendations.py`: Stock recommendations and sentiment analysis +2. **Module Organization**: Clear separation of concerns + - `trading/`: All trading-related operations + - `stock/`: Market data and analysis + - `models/`: Data structures only + - `http/`: Network communication -3. **Stock Module** (`src/py_alpaca_api/stock/`) - - `assets.py`: Asset information retrieval - - `history.py`: Historical stock data - - `screener.py`: Stock screening for gainers/losers - - `predictor.py`: Prophet-based stock prediction - - `latest_quote.py`: Real-time quote data +3. **Exception Hierarchy**: Custom exceptions for better error handling + ```python + PyAlpacaAPIError (base) + โ”œโ”€โ”€ AuthenticationError + โ”œโ”€โ”€ APIRequestError + โ””โ”€โ”€ ValidationError + ``` -4. **Models** (`src/py_alpaca_api/models/`) - - Dataclass models for all API entities - - `model_utils.py`: Utility functions for data transformation - - Consistent pattern: each model has a `from_dict()` function +4. **Type Safety**: Full type annotations throughout + ```python + def market( + self, + symbol: str, + qty: float | None = None, + notional: float | None = None, + side: str = "buy", + take_profit: float | None = None, + stop_loss: float | None = None, + ) -> OrderModel: + ``` -5. **HTTP Layer** (`src/py_alpaca_api/http/`) - - `requests.py`: Centralized HTTP request handling with retry logic - - Configurable retry strategies for resilient API communication +## ๐Ÿ”‘ API Authentication -### Key Design Patterns +### Environment Variables +```bash +# Required for all API operations +ALPACA_API_KEY=your_api_key_here +ALPACA_SECRET_KEY=your_secret_key_here + +# Optional - defaults to paper trading +ALPACA_API_PAPER=true # Set to false for live trading +``` + +### Authentication Flow +1. API credentials are passed to `PyAlpacaAPI` constructor +2. Headers are set with authentication tokens +3. All requests include authentication headers +4. 401 errors raise `AuthenticationError` + +## ๐Ÿ“Š Data Flow + +### Request Flow +``` +User Code โ†’ PyAlpacaAPI โ†’ Trading/Stock Module โ†’ HTTP Layer โ†’ Alpaca API + โ†“ +User Code โ† Model Object โ† from_dict() โ† JSON Response +``` + +### Model Conversion +1. API returns JSON response +2. `extract_class_data()` processes raw data +3. `from_dict()` creates typed model instance +4. Model returned to user with full type safety + +## ๐Ÿงช Testing Guidelines + +### Test Organization +- **Unit Tests**: Test individual functions/methods +- **Integration Tests**: Test API interactions +- **Mock Tests**: Use when API calls should be avoided + +### Writing Tests +```python +# Use fixtures for common setup +@pytest.fixture +def alpaca(): + return PyAlpacaAPI( + api_key=os.environ.get("ALPACA_API_KEY"), + api_secret=os.environ.get("ALPACA_SECRET_KEY"), + api_paper=True + ) + +# Test naming convention +def test_feature_scenario_expected_result(alpaca): + # Arrange + symbol = "AAPL" + + # Act + result = alpaca.stock.assets.get(symbol) + + # Assert + assert result.symbol == symbol +``` + +### Test Data +- Use paper trading account for all tests +- Clean up test data after each test (cancel orders, etc.) +- Use small quantities/notional values to avoid account limits + +## ๐Ÿ› Common Issues & Solutions + +### Issue: ValidationError instead of ValueError +**Solution**: Use `ValidationError` from `exceptions.py` for input validation + +### Issue: DataFrame type issues with pandas +**Solution**: Use explicit type assertions and `.copy()` to maintain DataFrame type +```python +df = df.loc[filter].copy() +assert isinstance(df, pd.DataFrame) +``` + +### Issue: Prophet seasonality parameters +**Solution**: Use "auto" string instead of boolean values +```python +yearly_seasonality="auto" # Not True/False +``` + +### Issue: API returns different column counts +**Solution**: Handle dynamic columns gracefully +```python +if len(df.columns) >= expected_cols: + df = df[expected_columns] +``` + +## ๐Ÿš€ Best Practices + +### Code Style +1. **Imports**: Use absolute imports from `py_alpaca_api` +2. **Type Hints**: Always include type annotations +3. **Docstrings**: Use Google style docstrings +4. **Line Length**: Maximum 88 characters (ruff default) +5. **Naming**: Use descriptive names, avoid abbreviations + +### Error Handling +```python +# Good +try: + result = api_call() +except APIRequestError as e: + logger.error(f"API request failed: {e}") + raise + +# Bad +try: + result = api_call() +except Exception: + pass # Never silent fail +``` + +### DataFrame Operations +```python +# Good - Preserve DataFrame type +df = df.loc[df["column"] > value].copy() + +# Bad - May return Series +df = df[df["column"] > value] +``` + +### API Calls +1. Always handle rate limiting +2. Use paper trading for development +3. Validate inputs before API calls +4. Log API errors for debugging + +## ๐Ÿ“ฆ Dependencies + +### Core Dependencies +- **pandas**: DataFrame operations, data analysis +- **numpy**: Numerical computations +- **requests**: HTTP client +- **pendulum**: Timezone-aware datetime handling +- **prophet**: Time series forecasting +- **yfinance**: Additional market data +- **beautifulsoup4**: HTML parsing for news + +### Development Dependencies +- **pytest**: Testing framework +- **pytest-cov**: Coverage reporting +- **pytest-mock**: Mocking support +- **ruff**: Linting and formatting +- **mypy**: Static type checking +- **pre-commit**: Git hooks +- **hypothesis**: Property-based testing + +## ๐Ÿ”„ CI/CD Pipeline + +### GitHub Actions Workflow +1. **Triggered by**: Push to any branch, PRs to main +2. **Steps**: + - Checkout code + - Set up Python 3.10+ + - Install dependencies + - Run linting (ruff) + - Run type checking (mypy) + - Run tests with coverage + - Upload coverage reports + +### Pre-commit Hooks +- `trailing-whitespace`: Remove trailing whitespace +- `end-of-file-fixer`: Ensure files end with newline +- `check-yaml`: Validate YAML files +- `check-json`: Validate JSON files +- `check-toml`: Validate TOML files +- `ruff`: Lint Python code +- `ruff-format`: Format Python code +- `mypy`: Type check Python code + +## ๐Ÿ“ˆ Performance Considerations + +1. **Rate Limiting**: Alpaca API has rate limits, use caching when possible +2. **Batch Operations**: Combine multiple requests when feasible +3. **DataFrame Operations**: Use vectorized operations over loops +4. **Prophet Models**: Cache trained models for repeated predictions +5. **News Fetching**: Implement caching to avoid repeated scraping + +## ๐Ÿ”’ Security + +1. **Never commit credentials**: Use environment variables +2. **Validate user input**: Prevent injection attacks +3. **Use paper trading**: For development and testing +4. **Secure storage**: Use proper secret management in production +5. **API key rotation**: Regularly rotate API keys + +## ๐Ÿ“š Additional Resources + +- [Alpaca API Documentation](https://alpaca.markets/docs/api-references/) +- [Prophet Documentation](https://facebook.github.io/prophet/) +- [Pandas Documentation](https://pandas.pydata.org/docs/) +- [Ruff Documentation](https://docs.astral.sh/ruff/) +- [MyPy Documentation](https://mypy.readthedocs.io/) + +## ๐ŸŽ“ Learning Path + +For new contributors: +1. Read the README.md for user perspective +2. Set up development environment +3. Run existing tests to understand functionality +4. Make small changes and run quality checks +5. Review existing code for patterns +6. Start with bug fixes before features + +## ๐Ÿ’ก Tips for AI Assistants + +1. **Always run tests** after making changes +2. **Use type hints** in all new code +3. **Follow existing patterns** in the codebase +4. **Check pre-commit hooks** before committing +5. **Update tests** when changing functionality +6. **Document breaking changes** clearly +7. **Preserve backward compatibility** when possible +8. **Use descriptive commit messages** + +--- -1. **Modular Architecture**: Each domain (trading, stock, models) is self-contained -2. **Dataclass Models**: All API responses are converted to typed dataclasses -3. **Centralized HTTP**: Single point for API communication with built-in resilience -4. **Factory Pattern**: `from_dict()` methods for model instantiation - -### API Authentication -- Requires `ALPACA_API_KEY` and `ALPACA_SECRET_KEY` environment variables -- Supports both paper and live trading environments -- Test script includes default paper trading credentials - -### External Dependencies -- **Data Processing**: pandas, numpy -- **Time Handling**: pendulum -- **Stock Prediction**: prophet -- **Web Scraping**: beautifulsoup4 -- **Market Data**: yfinance -- **HTTP**: requests with caching and rate limiting - -## Testing Approach - -Tests are organized by module: -- `test_http/`: HTTP request handling tests -- `test_models/`: Model creation and transformation tests -- `test_stock/`: Stock module functionality tests -- `test_trading/`: Trading operations tests - -All tests require API credentials (can use paper trading credentials). \ No newline at end of file +*Last Updated: Version 2.2.0* +*Maintained by: py-alpaca-api team* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f09f02a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,185 @@ +# Contributing to py-alpaca-api + +Thank you for your interest in contributing to py-alpaca-api! This guide will help you get started. + +## Development Setup + +### Prerequisites +- Python 3.10 or higher +- [uv](https://github.com/astral-sh/uv) package manager + +### Getting Started + +1. Fork and clone the repository: +```bash +git clone https://github.com/yourusername/py-alpaca-api.git +cd py-alpaca-api +``` + +2. Install dependencies: +```bash +make install +``` + +3. Set up pre-commit hooks: +```bash +make pre-commit +``` + +4. Set up environment variables: +```bash +export ALPACA_API_KEY=your_api_key +export ALPACA_SECRET_KEY=your_secret_key +``` + +## Development Workflow + +### Code Quality Tools + +We use several tools to maintain code quality: + +- **Ruff**: For linting and formatting +- **MyPy**: For static type checking +- **Pre-commit**: For automatic checks before commits + +### Common Commands + +Use the Makefile for common development tasks: + +```bash +# Run tests +make test + +# Format code +make format + +# Run linter +make lint + +# Fix linting issues +make lint-fix + +# Run type checker +make type-check + +# Run all checks +make check + +# Clean build artifacts +make clean +``` + +### Testing + +Run tests with: +```bash +./test.sh # Runs all tests with API credentials +make test # Alternative way +``` + +Run specific tests: +```bash +./test.sh tests/test_trading/test_orders.py +``` + +Generate coverage report: +```bash +make coverage +``` + +### Code Style + +- Follow PEP 8 guidelines +- Use type hints for all function signatures +- Write docstrings for all public functions and classes (Google style) +- Keep line length under 88 characters +- Use meaningful variable and function names + +### Pre-commit Hooks + +Pre-commit hooks will automatically run before each commit to ensure code quality: + +- Trailing whitespace removal +- End-of-file fixing +- YAML/JSON/TOML validation +- Ruff linting and formatting +- MyPy type checking + +If pre-commit fails, fix the issues and try committing again. + +## Making Changes + +### Workflow + +1. Create a new branch: +```bash +git checkout -b feature/your-feature-name +``` + +2. Make your changes and ensure tests pass: +```bash +make check +make test +``` + +3. Commit your changes: +```bash +git add . +git commit -m "feat: add new feature" +``` + +4. Push to your fork: +```bash +git push origin feature/your-feature-name +``` + +5. Create a pull request + +### Commit Messages + +Follow conventional commit format: +- `feat:` New feature +- `fix:` Bug fix +- `docs:` Documentation changes +- `style:` Code style changes +- `refactor:` Code refactoring +- `test:` Test changes +- `chore:` Maintenance tasks + +## Pull Request Process + +1. Ensure all tests pass +2. Update documentation if needed +3. Add tests for new functionality +4. Ensure your code follows the project style +5. Update the changelog if applicable +6. Request review from maintainers + +## Project Structure + +``` +py-alpaca-api/ +โ”œโ”€โ”€ src/ +โ”‚ โ””โ”€โ”€ py_alpaca_api/ +โ”‚ โ”œโ”€โ”€ __init__.py # Main API entry point +โ”‚ โ”œโ”€โ”€ exceptions.py # Custom exceptions +โ”‚ โ”œโ”€โ”€ http/ # HTTP request handling +โ”‚ โ”œโ”€โ”€ models/ # Data models +โ”‚ โ”œโ”€โ”€ stock/ # Stock market operations +โ”‚ โ””โ”€โ”€ trading/ # Trading operations +โ”œโ”€โ”€ tests/ # Test files +โ”œโ”€โ”€ docs/ # Documentation +โ”œโ”€โ”€ Makefile # Development commands +โ”œโ”€โ”€ pyproject.toml # Project configuration +โ””โ”€โ”€ .pre-commit-config.yaml # Pre-commit hooks +``` + +## Getting Help + +- Open an issue for bugs or feature requests +- Join discussions in existing issues +- Ask questions in pull requests + +## License + +By contributing, you agree that your contributions will be licensed under the same license as the project. diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index 11ad12a..0000000 --- a/GEMINI.md +++ /dev/null @@ -1,113 +0,0 @@ -# GEMINI.md - -This file provides guidance to Gemini (gemini.google.com) when working with code in this repository. - -## Development Commands - -### Package Management -This project uses `uv` for dependency management (migrated from Poetry). - -```bash -# Install dependencies -uv sync --all-extras --dev - -# Add a new dependency -uv add - -# Add a dev dependency -uv add --dev -``` - -### Testing -```bash -# Run all tests -uv run pytest tests - -# Run tests quietly -uv run pytest -q tests - -# Run specific test file -uv run pytest tests/test_trading/test_orders.py - -# Run with test script (includes API keys) -./test.sh - -# Run specific tests with test script -./test.sh tests/test_trading/test_orders.py -``` - -### Code Quality -```bash -# Run linter -uv run ruff check - -# Run linter with auto-fix -uv run ruff check --fix - -# Format code -uv run ruff format -``` - -## Architecture Overview - -### Core API Structure -The repository implements a Python wrapper for the Alpaca Trading API with the following architecture: - -1. **Main Entry Point**: `PyAlpacaAPI` class in `src/py_alpaca_api/__init__.py` - - Initializes with API key, secret, and paper trading flag - - Provides access to `trading` and `stock` modules - -2. **Trading Module** (`src/py_alpaca_api/trading/`) - - `account.py`: Account information, activities, and portfolio history - - `orders.py`: Order management and execution - - `positions.py`: Position tracking and management - - `watchlists.py`: Watchlist CRUD operations - - `market.py`: Market clock and calendar data - - `news.py`: Financial news from Yahoo Finance and Benzinga - - `recommendations.py`: Stock recommendations and sentiment analysis - -3. **Stock Module** (`src/py_alpaca_api/stock/`) - - `assets.py`: Asset information retrieval - - `history.py`: Historical stock data - - `screener.py`: Stock screening for gainers/losers - - `predictor.py`: Prophet-based stock prediction - - `latest_quote.py`: Real-time quote data - -4. **Models** (`src/py_alpaca_api/models/`) - - Dataclass models for all API entities - - `model_utils.py`: Utility functions for data transformation - - Consistent pattern: each model has a `from_dict()` function - -5. **HTTP Layer** (`src/py_alpaca_api/http/`) - - `requests.py`: Centralized HTTP request handling with retry logic - - Configurable retry strategies for resilient API communication - -### Key Design Patterns - -1. **Modular Architecture**: Each domain (trading, stock, models) is self-contained -2. **Dataclass Models**: All API responses are converted to typed dataclasses -3. **Centralized HTTP**: Single point for API communication with built-in resilience -4. **Factory Pattern**: `from_dict()` methods for model instantiation - -### API Authentication -- Requires `ALPACA_API_KEY` and `ALPACA_SECRET_KEY` environment variables -- Supports both paper and live trading environments -- Test script includes default paper trading credentials - -### External Dependencies -- **Data Processing**: pandas, numpy -- **Time Handling**: pendulum -- **Stock Prediction**: prophet -- **Web Scraping**: beautifulsoup4 -- **Market Data**: yfinance -- **HTTP**: requests with caching and rate limiting - -## Testing Approach - -Tests are organized by module: -- `test_http/`: HTTP request handling tests -- `test_models/`: Model creation and transformation tests -- `test_stock/`: Stock module functionality tests -- `test_trading/`: Trading operations tests - -All tests require API credentials (can use paper trading credentials). diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dd412a2 --- /dev/null +++ b/Makefile @@ -0,0 +1,99 @@ +.PHONY: help install test lint format type-check check clean pre-commit coverage docs + +# Default target - show help +help: + @echo "Available commands:" + @echo " make install - Install all dependencies including dev dependencies" + @echo " make test - Run all tests" + @echo " make test-quick - Run tests without coverage" + @echo " make lint - Run ruff linter" + @echo " make format - Format code with ruff" + @echo " make type-check - Run mypy type checker" + @echo " make check - Run all checks (lint, format-check, type-check)" + @echo " make clean - Remove build artifacts and cache files" + @echo " make pre-commit - Install and run pre-commit hooks" + @echo " make coverage - Generate test coverage report" + @echo " make docs - Build documentation" + +# Install all dependencies +install: + uv sync --all-extras --dev + +# Run tests with coverage +test: + ./test.sh + +# Run tests without coverage (faster) +test-quick: + uv run pytest -q tests + +# Run specific test file +test-file: + @read -p "Enter test file path: " file; \ + ./test.sh $$file + +# Run linter +lint: + uv run ruff check src + +# Fix linting issues automatically +lint-fix: + uv run ruff check --fix src + +# Format code +format: + uv run ruff format src + +# Check if code is formatted correctly +format-check: + uv run ruff format --check src + +# Run type checker +type-check: + uv run mypy src/ + +# Run all checks +check: format-check lint type-check + +# Clean build artifacts and cache +clean: + rm -rf build/ + rm -rf dist/ + rm -rf *.egg-info + rm -rf .pytest_cache + rm -rf .mypy_cache + rm -rf .ruff_cache + rm -rf htmlcov/ + rm -rf .coverage + rm -rf coverage.xml + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete + find . -type f -name "*.pyo" -delete + +# Install and run pre-commit hooks +pre-commit: + uv run pre-commit install + uv run pre-commit run --all-files + +# Update pre-commit hooks +pre-commit-update: + uv run pre-commit autoupdate + +# Generate coverage report +coverage: + uv run pytest --cov=py_alpaca_api --cov-report=html --cov-report=term-missing tests + @echo "Coverage report generated in htmlcov/index.html" + +# Build documentation (if you have sphinx or similar) +docs: + @echo "Documentation building not configured yet" + +# Development workflow - format, lint, and test +dev: format lint-fix test-quick + +# CI workflow - all checks without modifications +ci: check test + +# Release preparation +release-prep: clean check test + @echo "Ready for release!" diff --git a/README.md b/README.md index 6b6545d..bef0991 100644 --- a/README.md +++ b/README.md @@ -1,334 +1,403 @@ -

- project-logo -

-

-

PY-ALPACA-API

-

-

- Streamline Trading with Seamless Alpaca Integration -

-

- GitHub Actions Workflow Status - license - last-commit - repo-top-language - repo-language-count -

-

- Developed with the software and tools below. -

-

- tqdm - precommit - Poetry - Plotly - Python - GitHub%20Actions - pandas -

+# ๐Ÿš€ py-alpaca-api -
-
- Table of Contents
+[![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) +[![PyPI Version](https://img.shields.io/pypi/v/py-alpaca-api)](https://pypi.org/project/py-alpaca-api/) +[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[![Tests](https://github.com/TexasCoding/py-alpaca-api/workflows/CI/badge.svg)](https://github.com/TexasCoding/py-alpaca-api/actions) +[![Code Style](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff) +[![Type Checked](https://img.shields.io/badge/type_checked-mypy-blue)](http://mypy-lang.org/) -- [ Overview](#-overview) -- [ Features](#-features) -- [ Repository Structure](#-repository-structure) -- [ Modules](#-modules) -- [ Getting Started](#-getting-started) - - [ Installation](#-installation) - - [ Usage](#-usage) - - [ Tests](#-tests) -- [ Project Roadmap](#-project-roadmap) -- [ Contributing](#-contributing) -- [ License](#-license) -- [ Acknowledgments](#-acknowledgments) -
-
+A modern Python wrapper for the Alpaca Trading API, providing easy access to trading, market data, and account management functionality with full type safety and comprehensive testing. -## Overview +## โœจ Features -### Version 2.x is not compatible with previous versions. -Use the [V1.0.3](https://github.com/TexasCoding/py-alpaca-api/tree/master) branch for the previous version. +- **๐Ÿ” Complete Alpaca API Coverage**: Trading, market data, account management, and more +- **๐Ÿ“Š Stock Market Analysis**: Built-in screeners for gainers/losers, historical data analysis +- **๐Ÿค– ML-Powered Predictions**: Stock price predictions using Facebook Prophet +- **๐Ÿ“ฐ Financial News Integration**: Real-time news from Yahoo Finance and Benzinga +- **๐Ÿ“ˆ Technical Analysis**: Stock recommendations and sentiment analysis +- **๐ŸŽฏ Type Safety**: Full type annotations with mypy strict mode +- **๐Ÿงช Battle-Tested**: 100+ tests with comprehensive coverage +- **โšก Modern Python**: Async-ready, Python 3.10+ with latest best practices -The py-alpaca-api project provides a comprehensive Python interface for executing financial trading operations via the Alpaca API. It enables the management of watchlists, account positions, market data, and stock portfolios. It includes functionalities for order processing, stock screening, and predictive analytics leveraging historical data, enhancing market analysis and trading efficiencies. By abstracting complex API interactions into user-friendly Python modules, the project supports streamlined, data-driven trading decisions, making it a valuable tool for both developers and traders aiming for effective financial market engagement. +## ๐Ÿ“ฆ Installation -This project is mainly for fun and my personal use. Hopefully others find it helpful as well. Alpaca has a great Python SDK that provides a robust API interface, just more complex than I need for my uses. Checkout it out here [Alpaca-py](https://alpaca.markets/sdks/python/). +### Using pip ---- +```bash +pip install py-alpaca-api +``` -## Features - -| | Feature | Description | -|----|-------------------|---------------------------------------------------------------| -| โš™๏ธ | **Architecture** | The project is organized into modular packages, primarily dealing with stock trading, interactions with APIs (mainly Alpaca), and data handling. The trading modules handle various operations like watchlists, positions, accounts, news, and market interactions. | -| ๐Ÿ”ฉ | **Code Quality** | The codebase appears to follow a structured and modular approach with the usage of dataclasses for models ensuring clarity. The presence of utility functions indicates clean separation of concerns for data transformation tasks. | -| ๐Ÿ“„ | **Documentation** | Documentation includes code comments and descriptive docstrings for functions and classes. The `pyproject.toml` and `requirements.txt` files provide clear dependency management information. However, project-wide documentation and usage examples may need enhancement. | -| ๐Ÿ”Œ | **Integrations** | The code integrates with prominent financial data services like Yahoo Finance and Benzinga. It also utilizes Prophet for stock prediction and leverages the Alpaca trading API for executing trading operations. Matplotlib and Plotly are employed for data visualization. | -| ๐Ÿงฉ | **Modularity** | The project is highly modular with distinct packages and sub-packages handling specific responsibilities such as historical data retrieval, predictive analysis, trading functions, and account management. Reusability is evident through the use of utility modules. | -| ๐Ÿงช | **Testing** | Utilizes continuous integration via GitHub Actions, as seen in the `.github/workflows/test-package.yaml` workflow file. Testing practices appear to include automated tests for multiple environments which help catch issues early in the development process. | -| โšก๏ธ | **Performance** | Performance optimization measures include efficient HTTP request handling with retry mechanisms. The Prophet model ensures efficient stock prediction by leveraging historical data with advanced forecasting techniques. Explicit attention to modular detailed design suggests minimalistic performance overheads. | -| ๐Ÿ›ก๏ธ | **Security** | Security measures such as data validation within utility functions and thorough modeling for user and trading data are in place. However, explicit security practices regarding API key management or data encryption could be better detailed. | -| ๐Ÿ“ฆ | **Dependencies** | Key external libraries include `pandas` for data manipulation, `requests` for HTTP communication, `matplotlib` and `plotly` for visualization, `beautifulsoup4` for web scraping, `numpy` for numerical operations, and `prophet` for predictive modeling. | -| ๐Ÿš€ | **Scalability** | The architecture supports scalable operations given its modularity and use of robust libraries like `pandas` and `numpy`. The reliance on scalable cloud-hosted APIs such as Alpaca further enhances the capability to handle increased load. | +### Using uv (recommended) ---- +```bash +uv add py-alpaca-api +``` -## Repository Structure - -```sh -โ””โ”€โ”€ py-alpaca-api/ - โ”œโ”€โ”€ src - โ”‚ โ””โ”€โ”€ py_alpaca_api - โ”‚ โ”œโ”€โ”€ __init__.py - โ”‚ โ”œโ”€โ”€ http - โ”‚ โ”‚ โ””โ”€โ”€ requests.py - โ”‚ โ”œโ”€โ”€ models - โ”‚ โ”‚ โ”œโ”€โ”€ account_activity_model.py - โ”‚ โ”‚ โ”œโ”€โ”€ account_model.py - โ”‚ โ”‚ โ”œโ”€โ”€ asset_model.py - โ”‚ โ”‚ โ”œโ”€โ”€ clock_model.py - โ”‚ โ”‚ โ”œโ”€โ”€ model_utils.py - โ”‚ โ”‚ โ”œโ”€โ”€ order_model.py - โ”‚ โ”‚ โ”œโ”€โ”€ position_model.py - โ”‚ โ”‚ โ”œโ”€โ”€ quote_model.py - โ”‚ โ”‚ โ””โ”€โ”€ watchlist_model.py - โ”‚ โ”œโ”€โ”€ stock - โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py - โ”‚ โ”‚ โ”œโ”€โ”€ assets.py - โ”‚ โ”‚ โ”œโ”€โ”€ history.py - โ”‚ โ”‚ โ”œโ”€โ”€ latest_quote.py - โ”‚ โ”‚ โ”œโ”€โ”€ predictor.py - โ”‚ โ”‚ โ””โ”€โ”€ screener.py - โ”‚ โ””โ”€โ”€ trading - โ”‚ โ”œโ”€โ”€ __init__.py - โ”‚ โ”œโ”€โ”€ account.py - โ”‚ โ”œโ”€โ”€ market.py - โ”‚ โ”œโ”€โ”€ news.py - โ”‚ โ”œโ”€โ”€ orders.py - โ”‚ โ”œโ”€โ”€ positions.py - โ”‚ โ”œโ”€โ”€ recommendations.py - โ”‚ โ””โ”€โ”€ watchlists.py - โ””โ”€โ”€ tests - โ”œโ”€โ”€ __init__.py - โ”œโ”€โ”€ test_http - โ”‚ โ””โ”€โ”€ test_requests.py - โ”œโ”€โ”€ test_models - โ”‚ โ”œโ”€โ”€ test_account_activity_model.py - โ”‚ โ”œโ”€โ”€ test_account_model.py - โ”‚ โ”œโ”€โ”€ test_asset_model.py - โ”‚ โ”œโ”€โ”€ test_clock_model.py - โ”‚ โ”œโ”€โ”€ test_order_model.py - โ”‚ โ”œโ”€โ”€ test_position_model.py - โ”‚ โ””โ”€โ”€ test_watchlist_model.py - โ”œโ”€โ”€ test_stock - โ”‚ โ”œโ”€โ”€ test_assets.py - โ”‚ โ”œโ”€โ”€ test_history.py - โ”‚ โ”œโ”€โ”€ test_history2.py - โ”‚ โ”œโ”€โ”€ test_predictor.py - โ”‚ โ””โ”€โ”€ test_screener.py - โ””โ”€โ”€ test_trading - โ”œโ”€โ”€ test_account.py - โ”œโ”€โ”€ test_account2.py - โ”œโ”€โ”€ test_news.py - โ”œโ”€โ”€ test_orders.py - โ”œโ”€โ”€ test_positions.py - โ”œโ”€โ”€ test_recommendations.py - โ”œโ”€โ”€ test_watchlists.py - โ””โ”€โ”€ test_watchlists2.py +### Development Installation + +```bash +# Clone the repository +git clone https://github.com/TexasCoding/py-alpaca-api.git +cd py-alpaca-api + +# Install with development dependencies using uv +uv sync --all-extras --dev + +# Or using pip +pip install -e ".[dev]" ``` ---- +## ๐Ÿš€ Quick Start -## Modules +### Basic Setup -
. +```python +from py_alpaca_api import PyAlpacaAPI -| File | Summary | -| --- | --- | -| [requirements.txt](https://github.com/TexasCoding/py-alpaca-api/blob/master/requirements.txt) | Specify all required dependencies for the `py-alpaca-api` project, ensuring compatibility with Python versions 3.12 to 4.0. Critical dependencies facilitate functionalities for data visualization, time series analysis, HTTP requests, date manipulation, and prophet, among others, reinforcing seamless integrations and optimal performance across various platforms and environments. | -| [pyproject.toml](https://github.com/TexasCoding/py-alpaca-api/blob/master/pyproject.toml) | Defines metadata and dependency management for the py-alpaca-api project using Poetry, ensuring compatibility and functionality with specified Python and library versions, alongside configuring development, testing, and documentation dependencies for streamlined project maintenance and collaboration. Serves as the foundational setup for the project's environment. | +# Initialize with your API credentials +api = PyAlpacaAPI( + api_key="YOUR_API_KEY", + api_secret="YOUR_SECRET_KEY", + api_paper=True # Use paper trading for testing +) -
+# Get account information +account = api.trading.account.get() +print(f"Account Balance: ${account.cash}") +print(f"Buying Power: ${account.buying_power}") +``` -
src.py_alpaca_api.trading +### Trading Operations + +```python +# Place a market order +order = api.trading.orders.market( + symbol="AAPL", + qty=1, + side="buy" +) +print(f"Order placed: {order.id}") + +# Place a limit order +limit_order = api.trading.orders.limit( + symbol="GOOGL", + qty=1, + side="buy", + limit_price=150.00 +) + +# Get all positions +positions = api.trading.positions.get_all() +for position in positions: + print(f"{position.symbol}: {position.qty} shares @ ${position.avg_entry_price}") + +# Cancel all open orders +api.trading.orders.cancel_all() +``` -| File | Summary | -| --- | --- | -| [watchlists.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/trading/watchlists.py) | Facilitates complete management of watchlists in the trading module, handling operations such as retrieval, creation, updating, deletion, and manipulation of assets, seamlessly integrating with HTTP requests and watchlist model handling for comprehensive API interaction. Part of a structured trading architecture within the py-alpaca-api repository. | -| [recommendations.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/trading/recommendations.py) | Provide stock recommendations and generate sentiment analysis for given symbols, integrating with external APIs and popular stock data sources like Yahoo Finance. Enhance trading strategy modules in the parent repositorys architecture, supporting informed investment decisions for users. | -| [positions.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/trading/positions.py) | Manage user positions, providing retrieval and organization of Alpaca trading account positions. Enhance data with comprehensive market details, sorting capabilities, and support for tracking cash positions alongside asset positions, ensuring accurate portfolio analysis and streamlined access to current trading statuses. | -| [orders.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/trading/orders.py) | Facilitates the creation, retrieval, updating, and deletion of trading orders via the Alpaca API, providing comprehensive order management functionalities including various order types and statuses for effective trading operations. | -| [news.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/trading/news.py) | Retrieves and processes financial news articles related to specific market symbols from sources like Yahoo Finance and Benzinga, integrating them into the trading module to provide real-time, relevant news updates. Enables article scraping, HTML stripping, content truncation, and organized presentation with options to filter by date and content presence. | -| [market.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/trading/market.py) | Facilitates interaction with the market data endpoints. Provides methods to retrieve the current market clock and market calendar within a specified date range, returning structured data. This is essential for ensuring the core trading functionality operates with accurate market timing, enhancing decision-making and automation capabilities. | -| [account.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/trading/account.py) | Manage user account information, activities, and portfolio history within the Alpaca API trading module. Offer seamless data retrieval, including user account details, activity logs filtered by type and date, and detailed portfolio history with configurable periods, timeframes, and intraday reporting, presented in a structured and analyzable format. | +### Market Data & Analysis + +```python +# Get historical stock data +history = api.stock.history.get( + symbol="TSLA", + start="2024-01-01", + end="2024-12-31" +) + +# Get real-time quote +quote = api.stock.latest_quote.get("MSFT") +print(f"MSFT Price: ${quote.ask_price}") + +# Screen for top gainers +gainers = api.stock.screener.gainers( + price_greater_than=10.0, + change_greater_than=5.0, + volume_greater_than=1000000 +) +print("Top Gainers:") +for stock in gainers.head(10).itertuples(): + print(f"{stock.symbol}: +{stock.change}%") + +# Screen for top losers +losers = api.stock.screener.losers( + price_greater_than=10.0, + change_less_than=-5.0, + volume_greater_than=1000000 +) +``` -
+### Stock Predictions with ML -
src.py_alpaca_api.stock +```python +# Predict future stock prices using Prophet +predictions = api.stock.predictor.predict( + symbol="AAPL", + days_to_predict=30, + forecast_days_back=365 +) -| File | Summary | -| --- | --- | -| [screener.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/stock/screener.py) | Streamlines the identification and filtering of stock market gainers and losers based on specific criteria such as price, change, volume, and trade count. Leverages Alpaca Data API to retrieve and evaluate stocks, efficiently categorizing them for further decision-making processes in trading applications. | -| [predictor.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/stock/predictor.py) | Predicts future stock gainers by leveraging historical stock data and the Prophet model for forecasting. Collects data on previous day losers, trains a model, and generates a forecast to identify stocks expected to yield high future returns, aiding in strategic stock trading decisions. | -| [history.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/stock/history.py) | Retrieve and preprocess historical stock data, ensuring the asset is a valid stock before fetching. Offer end-users rich, structured financial data in customizable parameters to aid in stock analysis within the overarching Alpaca API-based trading platform architecture. | -| [latest_quote.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/stock/latest_quote.py) | Retrieves the latest quote data for specified stock symbols from the Alpaca API, including ask and bid prices, sizes, and timestamps, enabling real-time market monitoring. | -| [assets.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/stock/assets.py) | Provide functionality for retrieving asset information from the Alpaca API. Supports fetching individual asset details and obtaining a filtered DataFrame of multiple assets, focusing on active, fractionable, and tradable US equities while excluding specified exchanges. Integrates with asset models to ensure data consistency. | +# Get prediction for specific date +future_price = predictions[predictions['ds'] == '2024-12-31']['yhat'].values[0] +print(f"Predicted AAPL price on 2024-12-31: ${future_price:.2f}") +``` -
+### Financial News & Sentiment -
src.py_alpaca_api.models +```python +# Get latest financial news +news = api.trading.news.get(symbol="NVDA") +for article in news[:5]: + print(f"- {article['headline']}") + print(f" Sentiment: {article.get('sentiment', 'N/A')}") -| File | Summary | -| --- | --- | -| [watchlist_model.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/models/watchlist_model.py) | Facilitates the conversion and management of watchlist data for the Alpaca API by defining the `WatchlistModel` data class, processing asset lists into `AssetModel` objects, and providing functions to transform raw data dictionaries into fully-formed `WatchlistModel` instances, thus ensuring compatibility with the repositorys overall architecture. | -| [position_model.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/models/position_model.py) | Model investor positions, capturing attributes like asset details, market value, and performance metrics. Ensure seamless data transformation through a utility function that converts dictionaries into structured PositionModel instances. Central to monitoring and analyzing financial portfolios within the broader repository focused on trading and stock data management. | -| [order_model.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/models/order_model.py) | Manages the definition and creation of order data models within the API context. Facilitates the processing, conversion, and organization of order-related information, supporting detailed order data extraction and representation in a standardized model crucial for trading operations and strategies in the parent repository. | -| [model_utils.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/models/model_utils.py) | Facilitates data extraction and transformation for various data models within the Alpaca API by providing utility functions to retrieve and process dictionary values. Ensures consistent and type-safe data parsing for integers, floats, strings, and dates, optimizing data handling across the repositorys different model layers. | -| [clock_model.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/models/clock_model.py) | Define a data model for market clock information, encapsulating the market status and key timestamps. Include functions for creating model instances from dictionaries, facilitating structured and efficient data handling within the broader API. | -| [asset_model.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/models/asset_model.py) | Provide a structured abstraction for financial asset data, utilizing dataclass to define essential asset attributes. Facilitate transformation of data dictionaries to asset model instances, aiding in seamless interaction and manipulation within the broader trading API ecosystem. Boosts integration efficiency with other models and API endpoints. | -| [account_model.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/models/account_model.py) | Define and structure the properties and behavior of account-related data within the context of the API. Enables conversion of dictionary data into AccountModel instances for seamless data management and interaction with Alpacaโ€™s trading platform. | -| [account_activity_model.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/models/account_activity_model.py) | Models account activity data in a structured format, enabling easy conversion from dictionary inputs. Facilitates efficient data encapsulation and retrieval for handling account-related events within the trading application. Integrates with existing model utilities for standardized processing and consistency within the repositoryโ€™s architecture. | -| [quote_model.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/models/quote_model.py) | Defines a data model for stock quotes, encapsulating attributes such as ask price, bid price, and timestamps, with functions to convert dictionary data into QuoteModel instances for structured data handling. | +# Get stock recommendations +recommendations = api.trading.recommendations.get_recommendations("META") +sentiment = api.trading.recommendations.get_sentiment("META") +print(f"META Sentiment: {sentiment}") +``` -
+### Portfolio Analysis + +```python +# Get portfolio history +portfolio_history = api.trading.account.portfolio_history( + period="1M", + timeframe="1D" +) + +# Calculate returns +returns = ( + (portfolio_history['equity'].iloc[-1] - portfolio_history['equity'].iloc[0]) / + portfolio_history['equity'].iloc[0] * 100 +) +print(f"Monthly Return: {returns:.2f}%") + +# Get account activities +activities = api.trading.account.get_activities() +for activity in activities: + print(f"{activity.created_at}: {activity.activity_type} - {activity.symbol}") +``` -
src.py_alpaca_api.http +## ๐Ÿ“Š Advanced Features -| File | Summary | -| --- | --- | -| [requests.py](https://github.com/TexasCoding/py-alpaca-api/blob/master/src/py_alpaca_api/http/requests.py) | Handle HTTP requests with configurable retry strategies, ensuring resilient communication with APIs, essential for robust data exchanges and integrations within the py-alpaca-api repository. | +### Watchlist Management -
+```python +# Create a watchlist +watchlist = api.trading.watchlists.create_watchlist( + name="Tech Stocks", + symbols=["AAPL", "GOOGL", "MSFT", "NVDA"] +) -
.github.workflows +# Add symbols to existing watchlist +api.trading.watchlists.add_assets_to_watchlist( + watchlist_id=watchlist.id, + symbols=["META", "AMZN"] +) -| File | Summary | -| --- | --- | -| [test-package.yaml](https://github.com/TexasCoding/py-alpaca-api/blob/master/.github/workflows/test-package.yaml) | Define the continuous integration workflow for the repository by automating the testing process. Configure multi-environment tests for the software package to ensure reliability and catch issues early by automatically running tests on every code push or pull request. | +# Get all watchlists +watchlists = api.trading.watchlists.get_all_watchlists() +``` -
+### Advanced Order Types + +```python +# Stop-loss order +stop_loss = api.trading.orders.stop( + symbol="TSLA", + qty=1, + side="sell", + stop_price=180.00 +) + +# Trailing stop order +trailing_stop = api.trading.orders.trailing_stop( + symbol="NVDA", + qty=1, + side="sell", + trail_percent=5.0 # 5% trailing stop +) + +# One-Cancels-Other (OCO) order +oco_order = api.trading.orders.market( + symbol="AAPL", + qty=1, + side="buy", + take_profit=200.00, + stop_loss=150.00 +) +``` ---- +### Market Hours & Calendar -## Getting Started - -**System Requirements:** - -* **Python**: `version 3.12.0` - -### Installation - -

From source

- -> 1. Clone the py-alpaca-api repository: -> -> ```console -> $ git clone https://github.com/TexasCoding/py-alpaca-api -> ``` -> -> 2. Change to the project directory: -> ```console -> $ cd py-alpaca-api -> ``` -> -> 3. Install the dependencies: -> ```console -> $ pip install -r requirements.txt -> ``` - -### Usage - -

From source

- -> Run py-alpaca-api using the command below: -> ```python -> import os -> from py_alpaca_api import PyAlpacaAPI -> -> api_key = os.environ.get("ALPACA_API_KEY") -> api_secret = os.environ.get("ALPACA_SECRET_KEY") -> -> api = PyAlpacaAPI(api_key=api_key, api_secret=api_secret) -> -> # Get the account information for the authenticated account. -> account = api.trading.account.get() -> -> # Get stock asset information -> asset = api.stock.assets.get("AAPL") -> -> # Get stock historical data -> historical_data = api.stock.history.get_stock_data("AAPL", start="2021-01-01", end="2021-01-10") -> ``` - -### Tests - -> Run the test suite using the command below: -> Export your API key and secret key as environment variables: -> Or use .env file (recommended) -> ```console -> $ export ALPACA_API_KEY="YOUR_API_KEY" -> $ export ALPACA_SECRET_KEY="YOUR_SECRET_KEY" -> -> $ pytest -> ``` - - -## Contributing - -Contributions are welcome! Here are several ways you can contribute: - -- **[Report Issues](https://github.com/TexasCoding/py-alpaca-api/issues)**: Submit bugs found or log feature requests for the `py-alpaca-api` project. -- **[Submit Pull Requests](https://github.com/TexasCoding/py-alpaca-api/blob/main/CONTRIBUTING.md)**: Review open PRs, and submit your own PRs. -- **[Join the Discussions](https://github.com/TexasCoding/py-alpaca-api/discussions)**: Share your insights, provide feedback, or ask questions. - -
-Contributing Guidelines - -1. **Fork the Repository**: Start by forking the project repository to your github account. -2. **Clone Locally**: Clone the forked repository to your local machine using a git client. - ```sh - git clone https://github.com/TexasCoding/py-alpaca-api - ``` -3. **Create a New Branch**: Always work on a new branch, giving it a descriptive name. - ```sh - git checkout -b new-feature-x - ``` -4. **Make Your Changes**: Develop and test your changes locally. -5. **Commit Your Changes**: Commit with a clear message describing your updates. - ```sh - git commit -m 'Implemented new feature x.' - ``` -6. **Push to github**: Push the changes to your forked repository. - ```sh - git push origin new-feature-x - ``` -7. **Submit a Pull Request**: Create a PR against the original project repository. Clearly describe the changes and their motivations. -8. **Review**: Once your PR is reviewed and approved, it will be merged into the main branch. Congratulations on your contribution! -
- -
-Contributor Graph -
-

- - - -

-
+```python +# Check if market is open +clock = api.trading.market.clock() +print(f"Market is {'open' if clock.is_open else 'closed'}") +print(f"Next open: {clock.next_open}") +print(f"Next close: {clock.next_close}") ---- +# Get market calendar +calendar = api.trading.market.calendar( + start_date="2024-01-01", + end_date="2024-12-31" +) +``` -## License +## ๐Ÿงช Testing -This project is protected under the [MIT](https://choosealicense.com/licenses/mit/) License. For more details, refer to the [LICENSE](https://choosealicense.com/licenses/mit/) file. +The project includes comprehensive test coverage. Run tests using: ---- +```bash +# Run all tests +./test.sh + +# Run specific test file +./test.sh tests/test_trading/test_orders.py + +# Run with coverage +uv run pytest --cov=py_alpaca_api --cov-report=html + +# Run with markers +uv run pytest -m "not slow" # Skip slow tests +``` -## Acknowledgments +## ๐Ÿ› ๏ธ Development -- List any resources, contributors, inspiration, etc. here. +### Setup Development Environment -[**Return**](#-overview) +```bash +# Install development dependencies +uv sync --all-extras --dev + +# Install pre-commit hooks +pre-commit install + +# Run code quality checks +make check + +# Format code +make format + +# Run type checking +make type-check + +# Run linting +make lint +``` + +### Code Quality Tools + +- **Ruff**: Fast Python linter and formatter +- **MyPy**: Static type checker with strict mode +- **Pre-commit**: Git hooks for code quality +- **Pytest**: Testing framework with coverage + +### Project Structure + +``` +py-alpaca-api/ +โ”œโ”€โ”€ src/py_alpaca_api/ +โ”‚ โ”œโ”€โ”€ __init__.py # Main API client +โ”‚ โ”œโ”€โ”€ exceptions.py # Custom exceptions +โ”‚ โ”œโ”€โ”€ trading/ # Trading operations +โ”‚ โ”‚ โ”œโ”€โ”€ account.py # Account management +โ”‚ โ”‚ โ”œโ”€โ”€ orders.py # Order management +โ”‚ โ”‚ โ”œโ”€โ”€ positions.py # Position tracking +โ”‚ โ”‚ โ”œโ”€โ”€ watchlists.py # Watchlist operations +โ”‚ โ”‚ โ”œโ”€โ”€ market.py # Market data +โ”‚ โ”‚ โ”œโ”€โ”€ news.py # Financial news +โ”‚ โ”‚ โ””โ”€โ”€ recommendations.py # Stock analysis +โ”‚ โ”œโ”€โ”€ stock/ # Stock market data +โ”‚ โ”‚ โ”œโ”€โ”€ assets.py # Asset information +โ”‚ โ”‚ โ”œโ”€โ”€ history.py # Historical data +โ”‚ โ”‚ โ”œโ”€โ”€ screener.py # Stock screening +โ”‚ โ”‚ โ”œโ”€โ”€ predictor.py # ML predictions +โ”‚ โ”‚ โ””โ”€โ”€ latest_quote.py # Real-time quotes +โ”‚ โ”œโ”€โ”€ models/ # Data models +โ”‚ โ””โ”€โ”€ http/ # HTTP client +โ”œโ”€โ”€ tests/ # Test suite +โ”œโ”€โ”€ docs/ # Documentation +โ””โ”€โ”€ pyproject.toml # Project configuration +``` + +## ๐Ÿ“– Documentation + +Full documentation is available at [Read the Docs](https://py-alpaca-api.readthedocs.io) + +### API Reference + +- [Trading API](https://py-alpaca-api.readthedocs.io/en/latest/trading/) - Orders, positions, and account management +- [Market Data API](https://py-alpaca-api.readthedocs.io/en/latest/market_data/) - Historical and real-time data +- [Stock Analysis](https://py-alpaca-api.readthedocs.io/en/latest/analysis/) - Screeners, predictions, and sentiment +- [Models](https://py-alpaca-api.readthedocs.io/en/latest/models/) - Data models and type definitions + +## ๐Ÿค Contributing + +We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +### Development Guidelines + +- Write tests for new features +- Follow the existing code style (enforced by ruff) +- Add type hints to all functions +- Update documentation as needed +- Ensure all tests pass before submitting PR + +## ๐Ÿ“„ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## ๐Ÿ™ Acknowledgments + +- [Alpaca Markets](https://alpaca.markets/) for providing the trading API +- [Prophet](https://facebook.github.io/prophet/) for time series forecasting +- [yfinance](https://github.com/ranaroussi/yfinance) for market data +- [Beautiful Soup](https://www.crummy.com/software/BeautifulSoup/) for web scraping +- All contributors who have helped improve this project + +## ๐Ÿ“ž Support + +- ๐Ÿ“ง **Issues**: [GitHub Issues](https://github.com/TexasCoding/py-alpaca-api/issues) +- ๐Ÿ’ฌ **Discussions**: [GitHub Discussions](https://github.com/TexasCoding/py-alpaca-api/discussions) +- ๐Ÿ“– **Wiki**: [GitHub Wiki](https://github.com/TexasCoding/py-alpaca-api/wiki) + +## ๐Ÿšฆ Project Status + +![Tests](https://github.com/TexasCoding/py-alpaca-api/workflows/CI/badge.svg) +![Python Version](https://img.shields.io/badge/python-3.10%2B-blue) +![Last Commit](https://img.shields.io/github/last-commit/TexasCoding/py-alpaca-api) +![Issues](https://img.shields.io/github/issues/TexasCoding/py-alpaca-api) + +## ๐Ÿ—บ๏ธ Roadmap + +- [ ] WebSocket support for real-time data streaming +- [ ] Options trading support +- [ ] Crypto trading integration +- [ ] Advanced portfolio analytics +- [ ] Backtesting framework +- [ ] Strategy automation tools +- [ ] Mobile app integration + +## โš ๏ธ Disclaimer + +This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. + +Always start with paper trading to test your strategies before using real money. --- + +

Made with โค๏ธ by the py-alpaca-api team

+

+ + GitHub Stars + +

diff --git a/docs/make.bat b/docs/make.bat index 747ffb7..dc1312a 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,35 +1,35 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py index 5d48ebc..e99a5ba 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,6 +1,5 @@ from pathlib import Path - project = "PyAlpacaAPI" copyright = "MIT 2024, TexasCoding" author = "TexasCoding" @@ -19,7 +18,7 @@ autoapi_dirs = [f"{Path(__file__).parents[2]}/src"] templates_path = ["_templates"] -exclude_patterns = [] +exclude_patterns: list[str] = [] html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] diff --git a/docs/source/notebooks/usage.ipynb b/docs/source/notebooks/usage.ipynb index 363ff86..b22874d 100644 --- a/docs/source/notebooks/usage.ipynb +++ b/docs/source/notebooks/usage.ipynb @@ -31,6 +31,7 @@ "source": [ "import os\n", "from pprint import pprint\n", + "\n", "from py_alpaca_api import PyAlpacaAPI\n", "\n", "# Load environment variables\n", diff --git a/pyproject.toml b/pyproject.toml index 01feba2..7d00e85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "py-alpaca-api" -version = "2.1.10" +version = "2.2.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.10" @@ -26,5 +26,158 @@ dev-dependencies = [ "pre-commit>=3.8.0", "pytest>=8.3.3", "pytest-mock>=3.14.0", + "pytest-cov>=5.0.0", "ruff>=0.6.8", + "mypy>=1.11.0", + "types-requests>=2.32.0", + "types-beautifulsoup4>=4.12.0", ] + +# Ruff configuration - Modern Python linter and formatter +[tool.ruff] +target-version = "py310" +line-length = 88 +fix = true + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "ARG", # flake8-unused-arguments + "SIM", # flake8-simplify + "PTH", # flake8-use-pathlib + "N", # pep8-naming + "D", # pydocstyle + "RUF", # Ruff-specific rules + "TRY", # tryceratops + "LOG", # flake8-logging + "RET", # flake8-return + "ERA", # eradicate + "PL", # Pylint +] +ignore = [ + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in __init__ + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "PLR0913", # Too many arguments to function call + "PLR0915", # Too many statements + "PLR2004", # Magic value used in comparison + "E501", # Line too long + "D205", + "TRY003", + "TRY002", + "D415", + "D301", + "RET504" +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["D", "S101", "ARG", "PLR2004"] +"__init__.py" = ["F401", "D104"] + +[tool.ruff.lint.isort] +known-first-party = ["py_alpaca_api"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +# MyPy configuration - Static type checker +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +disallow_any_unimported = false +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true +check_untyped_defs = true +ignore_missing_imports = true +show_error_codes = true +show_column_numbers = true +pretty = true + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_untyped_defs = false +check_untyped_defs = false + +[[tool.mypy.overrides]] +module = "py_alpaca_api.*" +strict = false +warn_return_any = false +no_implicit_optional = false + +[[tool.mypy.overrides]] +module = [ + "prophet.*", + "yfinance.*", + "requests_cache.*", + "requests_ratelimiter.*", + "pendulum.*", +] +ignore_missing_imports = true + +# Pytest configuration +[tool.pytest.ini_options] +minversion = "8.0" +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "-ra", + "--strict-markers", + "--cov=py_alpaca_api", + "--cov-report=term-missing", + "--cov-report=html", + "--cov-report=xml", + "--tb=short", # Shorter traceback format +] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "integration: marks tests as integration tests", + "unit: marks tests as unit tests", +] + +# Coverage configuration +[tool.coverage.run] +source = ["py_alpaca_api"] +branch = true +omit = [ + "*/tests/*", + "*/__init__.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if __name__ == .__main__.:", + "raise NotImplementedError", + "if TYPE_CHECKING:", +] +precision = 2 +show_missing = true +skip_covered = false diff --git a/src/py_alpaca_api/__init__.py b/src/py_alpaca_api/__init__.py index ebb1b70..97692cd 100644 --- a/src/py_alpaca_api/__init__.py +++ b/src/py_alpaca_api/__init__.py @@ -1,11 +1,12 @@ -from .trading import Trading +from .exceptions import AuthenticationError from .stock import Stock +from .trading import Trading class PyAlpacaAPI: def __init__(self, api_key: str, api_secret: str, api_paper: bool = True) -> None: if not api_key or not api_secret: - raise Exception("API Key and Secret are required") + raise AuthenticationError() self._initialize_components( api_key=api_key, api_secret=api_secret, api_paper=api_paper ) diff --git a/src/py_alpaca_api/exceptions.py b/src/py_alpaca_api/exceptions.py new file mode 100644 index 0000000..dd1ff31 --- /dev/null +++ b/src/py_alpaca_api/exceptions.py @@ -0,0 +1,48 @@ +"""Custom exceptions for the py-alpaca-api library.""" + + +class PyAlpacaAPIError(Exception): + """Base exception for all py-alpaca-api errors.""" + + pass + + +class AuthenticationError(PyAlpacaAPIError): + """Raised when API authentication fails.""" + + def __init__(self, message: str = "API Key and Secret are required"): + self.message = message + super().__init__(self.message) + + +class APIRequestError(PyAlpacaAPIError): + """Raised when an API request fails.""" + + def __init__(self, status_code: int | None = None, message: str = ""): + self.status_code = status_code + self.message = f"Request Error: {message}" if message else "Request Error" + super().__init__(self.message) + + +class ValidationError(PyAlpacaAPIError): + """Raised when input validation fails.""" + + pass + + +class OrderError(PyAlpacaAPIError): + """Raised when order operations fail.""" + + pass + + +class PositionError(PyAlpacaAPIError): + """Raised when position operations fail.""" + + pass + + +class DataError(PyAlpacaAPIError): + """Raised when data processing fails.""" + + pass diff --git a/src/py_alpaca_api/http/requests.py b/src/py_alpaca_api/http/requests.py index 4aca85c..bcc2d98 100644 --- a/src/py_alpaca_api/http/requests.py +++ b/src/py_alpaca_api/http/requests.py @@ -1,9 +1,11 @@ -from typing import Dict, Optional +from typing import Any import requests from requests.adapters import HTTPAdapter from urllib3.util import Retry +from ..exceptions import APIRequestError + class Requests: def __init__(self) -> None: @@ -20,23 +22,26 @@ def request( self, method: str, url: str, - headers: Optional[Dict[str, str]] = None, - params: Optional[Dict[str, str] | Dict[str, bool] | Dict[str, float]] = None, - json: Optional[Dict[str, str]] = None, + headers: dict[str, str] | None = None, + params: dict[str, str | bool | float | int] | None = None, + json: dict[str, Any] | None = None, ): - """ + """Execute HTTP request with retry logic. + Args: method: A string representing the HTTP method to be used in the request. url: A string representing the URL to send the request to. headers: An optional dictionary containing the headers for the request. - params: An optional dictionary containing the query parameters for the request. + params: An optional dictionary containing the query parameters for the + request. json: An optional dictionary containing the JSON payload for the request. Returns: The response object returned by the server. Raises: - Exception: If the response status code is not one of the acceptable statuses (200, 204, 207). + APIRequestError: If the response status code is not one of the + acceptable statuses (200, 204, 207). """ response = self.session.request( method=method, @@ -47,5 +52,7 @@ def request( ) acceptable_statuses = [200, 204, 207] if response.status_code not in acceptable_statuses: - raise Exception(f"Request Error: {response.text}") + raise APIRequestError( + status_code=response.status_code, message=response.text + ) return response diff --git a/src/py_alpaca_api/models/account_activity_model.py b/src/py_alpaca_api/models/account_activity_model.py index cf67964..f3b9f97 100644 --- a/src/py_alpaca_api/models/account_activity_model.py +++ b/src/py_alpaca_api/models/account_activity_model.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from datetime import datetime + from py_alpaca_api.models.model_utils import KEY_PROCESSORS, extract_class_data @@ -26,7 +27,8 @@ class AccountActivityModel: # Data Class Asset Conversion Functions ############################################ def account_activity_class_from_dict(data_dict: dict) -> AccountActivityModel: - """ + """Converts a dictionary into an instance of the `AccountActivityModel`. + Args: data_dict: A dictionary containing the data for creating an instance of AccountActivityModel. diff --git a/src/py_alpaca_api/models/account_model.py b/src/py_alpaca_api/models/account_model.py index 55bfd01..178070d 100644 --- a/src/py_alpaca_api/models/account_model.py +++ b/src/py_alpaca_api/models/account_model.py @@ -52,8 +52,7 @@ class AccountModel: # Data Class Account Conversion Functions ############################################ def account_class_from_dict(data_dict: dict) -> AccountModel: - """ - Converts a dictionary into an instance of the `AccountModel`. + """Converts a dictionary into an instance of the `AccountModel`. Args: data_dict (dict): A dictionary containing the data for the `AccountModel` instance. diff --git a/src/py_alpaca_api/models/asset_model.py b/src/py_alpaca_api/models/asset_model.py index e409040..5ad3795 100644 --- a/src/py_alpaca_api/models/asset_model.py +++ b/src/py_alpaca_api/models/asset_model.py @@ -23,9 +23,11 @@ class AssetModel: # Data Class Asset Conversion Functions ############################################ def asset_class_from_dict(data_dict: dict) -> AssetModel: - """ + """Create AssetModel from dictionary data. + Args: - data_dict: A dictionary containing the data for creating an instance of AssetModel. + data_dict: A dictionary containing the data for creating an instance of + AssetModel. Returns: An instance of the AssetModel class. diff --git a/src/py_alpaca_api/models/clock_model.py b/src/py_alpaca_api/models/clock_model.py index 6641db6..6972bf1 100644 --- a/src/py_alpaca_api/models/clock_model.py +++ b/src/py_alpaca_api/models/clock_model.py @@ -16,9 +16,11 @@ class ClockModel: # Data Class Clock Conversion Functions ############################################ def clock_class_from_dict(data_dict: dict) -> ClockModel: - """ + """Create ClockModel from dictionary data. + Args: - data_dict: A dictionary containing data for creating an instance of `ClockModel`. + data_dict: A dictionary containing data for creating an instance of + `ClockModel`. Returns: An instance of `ClockModel` created using the data from `data_dict`. diff --git a/src/py_alpaca_api/models/model_utils.py b/src/py_alpaca_api/models/model_utils.py index 7ba1cd9..bf78107 100644 --- a/src/py_alpaca_api/models/model_utils.py +++ b/src/py_alpaca_api/models/model_utils.py @@ -1,13 +1,11 @@ -from dataclasses import dataclass from datetime import datetime -from typing import Dict, List +from typing import Any import pendulum def get_dict_str_value(data_dict: dict, key: str) -> str: - """ - Returns the string value of a specific key within a dictionary. + """Returns the string value of a specific key within a dictionary. Args: data_dict (dict): The dictionary containing the data. @@ -20,30 +18,42 @@ def get_dict_str_value(data_dict: dict, key: str) -> str: return str(data_dict[key]) if data_dict.get(key) else "" -def parse_date(data_dict: dict, key: str) -> datetime: - """ - Parses a date value from a dictionary using a specified key. +def parse_date(data_dict: dict, key: str) -> str: + """Parses a date value from a dictionary using a specified key. Args: data_dict (dict): The dictionary from which to extract the date value. key (str): The key in the dictionary representing the date value. Returns: - datetime: The parsed date value as a `datetime` object. + str: The parsed date value as a formatted string. """ - return ( - pendulum.parse(data_dict[key], tz="America/New_York").strftime( - "%Y-%m-%d %H:%M:%S" - ) - if data_dict.get(key) - else pendulum.DateTime.min.strftime("%Y-%m-%d") - ) + if not data_dict.get(key): + return pendulum.DateTime.min.strftime("%Y-%m-%d") + + # Parse the date value - pendulum.parse can return various types, + # but we ensure it's a DateTime by explicitly converting + parsed_date = pendulum.parse(data_dict[key], tz="America/New_York") + + # Ensure we have a proper DateTime object + if isinstance(parsed_date, pendulum.DateTime): + return parsed_date.strftime("%Y-%m-%d %H:%M:%S") + # If parsing returns something else, try to handle it + try: + # Convert to datetime string and reparse as DateTime + datetime_obj = pendulum.parse(str(parsed_date), tz="America/New_York") + if isinstance(datetime_obj, pendulum.DateTime): + return datetime_obj.strftime("%Y-%m-%d %H:%M:%S") + # Fallback to minimum date + return pendulum.DateTime.min.strftime("%Y-%m-%d") + except Exception: + # If all else fails, return minimum date + return pendulum.DateTime.min.strftime("%Y-%m-%d") def get_dict_float_value(data_dict: dict, key: str) -> float: - """ - Args: + """Args: data_dict (dict): A dictionary containing the data. key (str): The key to look for in the data_dict. @@ -55,8 +65,7 @@ def get_dict_float_value(data_dict: dict, key: str) -> float: def get_dict_int_value(data_dict: dict, key: str) -> int: - """ - Args: + """Args: data_dict: A dictionary containing key-value pairs. key: The key whose corresponding value is to be returned. @@ -72,22 +81,21 @@ def get_dict_int_value(data_dict: dict, key: str) -> int: str: get_dict_str_value, float: get_dict_float_value, datetime: parse_date, - bool: lambda data_dict, key: bool(data_dict[key]), - List[object]: lambda data_dict, key: (data_dict[key] if data_dict.get(key) else []), + bool: lambda data_dict, key: bool(data_dict.get(key, False)), + list[object]: lambda data_dict, key: (data_dict[key] if data_dict.get(key) else []), } ############################################ # Data Class Extraction Functions ############################################ -def extract_class_data(data_dict: dict, field_processors: Dict, data_class: dataclass): - """ - Extracts and processes data from a dictionary based on a given data class and field processors. +def extract_class_data(data_dict: dict, field_processors: dict, data_class: type[Any]): + """Extracts and processes data from a dictionary based on a given data class and field processors. Args: data_dict (dict): The dictionary containing the data to be processed. field_processors (Dict): A dictionary of field processors. - data_class (dataclass): The data class used to define the fields and types. + data_class (type[Any]): The data class used to define the fields and types. Returns: dict: A dictionary containing processed data, with keys corresponding to the fields of the data class. @@ -101,5 +109,5 @@ def extract_class_data(data_dict: dict, field_processors: Dict, data_class: data return { field: field_processors[data_type](data_dict, field) for field, data_type in data_class.__annotations__.items() - if field_processors.get(data_type, None) + if field_processors.get(data_type) } diff --git a/src/py_alpaca_api/models/order_model.py b/src/py_alpaca_api/models/order_model.py index d2f9f3b..7c69a67 100644 --- a/src/py_alpaca_api/models/order_model.py +++ b/src/py_alpaca_api/models/order_model.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from datetime import datetime -from typing import Dict, List from py_alpaca_api.models.model_utils import KEY_PROCESSORS, extract_class_data @@ -35,7 +34,7 @@ class OrderModel: stop_price: float status: str extended_hours: bool - legs: List[object] + legs: list[object] trail_percent: float trail_price: float hwm: float @@ -46,9 +45,8 @@ class OrderModel: ############################################ # Data Class Order Conversion Functions ############################################ -def process_legs(legs: List[Dict]) -> List[OrderModel]: - """ - Process the legs and create a list of OrderModel objects based on the provided data. +def process_legs(legs: list[dict]) -> list[OrderModel]: + """Process the legs and create a list of OrderModel objects based on the provided data. Args: legs (List[Dict]): A list of dictionaries representing the legs. @@ -71,9 +69,8 @@ def process_legs(legs: List[Dict]) -> List[OrderModel]: ) -def order_class_from_dict(data_dict: Dict) -> OrderModel: - """ - Creates an instance of `OrderModel` using the provided dictionary data. +def order_class_from_dict(data_dict: dict) -> OrderModel: + """Creates an instance of `OrderModel` using the provided dictionary data. Args: data_dict (Dict): A dictionary containing the data used to create the `OrderModel` instance. diff --git a/src/py_alpaca_api/models/position_model.py b/src/py_alpaca_api/models/position_model.py index 80b2bfe..891767c 100644 --- a/src/py_alpaca_api/models/position_model.py +++ b/src/py_alpaca_api/models/position_model.py @@ -30,8 +30,7 @@ class PositionModel: # Data Class Position Conversion Functions ############################################ def position_class_from_dict(data_dict: dict) -> PositionModel: - """ - Returns a PositionModel object created from a given data dictionary. + """Returns a PositionModel object created from a given data dictionary. Args: data_dict: A dictionary containing the data for creating a PositionModel object. diff --git a/src/py_alpaca_api/models/quote_model.py b/src/py_alpaca_api/models/quote_model.py index b12bc19..01e6209 100644 --- a/src/py_alpaca_api/models/quote_model.py +++ b/src/py_alpaca_api/models/quote_model.py @@ -18,8 +18,7 @@ class QuoteModel: # Data Class Quote Conversion Functions ############################################ def quote_class_from_dict(data_dict: dict) -> QuoteModel: - """ - Args: + """Args: data_dict: A dictionary containing data for creating an instance of `QuoteModel`. Returns: diff --git a/src/py_alpaca_api/models/watchlist_model.py b/src/py_alpaca_api/models/watchlist_model.py index d4a041b..07262ea 100644 --- a/src/py_alpaca_api/models/watchlist_model.py +++ b/src/py_alpaca_api/models/watchlist_model.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from datetime import datetime -from typing import Dict, List, Union from py_alpaca_api.models.asset_model import AssetModel from py_alpaca_api.models.model_utils import KEY_PROCESSORS, extract_class_data @@ -13,13 +12,13 @@ class WatchlistModel: created_at: datetime updated_at: datetime name: str - assets: List[Union[object | AssetModel]] + assets: list[AssetModel] ############################################ # Data Class Watchlist Conversion Functions ############################################ -def process_assets(assets: List[Dict]) -> List[AssetModel]: +def process_assets(assets: list[dict]) -> list[AssetModel]: """Process a list of assets. This function takes a list of asset dictionaries and returns a list of AssetModel objects. @@ -44,8 +43,7 @@ def process_assets(assets: List[Dict]) -> List[AssetModel]: def watchlist_class_from_dict(data_dict: dict) -> WatchlistModel: - """ - Args: + """Args: data_dict: A dictionary containing the data needed to create a WatchlistModel object. Returns: diff --git a/src/py_alpaca_api/stock/__init__.py b/src/py_alpaca_api/stock/__init__.py index 1c8c9df..33c4b37 100644 --- a/src/py_alpaca_api/stock/__init__.py +++ b/src/py_alpaca_api/stock/__init__.py @@ -1,10 +1,9 @@ -from typing import Dict -from py_alpaca_api.stock.latest_quote import LatestQuote -from py_alpaca_api.stock.predictor import Predictor -from py_alpaca_api.trading.market import Market from py_alpaca_api.stock.assets import Assets from py_alpaca_api.stock.history import History +from py_alpaca_api.stock.latest_quote import LatestQuote +from py_alpaca_api.stock.predictor import Predictor from py_alpaca_api.stock.screener import Screener +from py_alpaca_api.trading.market import Market class Stock: @@ -35,7 +34,7 @@ def __init__( def _initialize_components( self, - headers: Dict[str, str], + headers: dict[str, str], base_url: str, data_url: str, market: Market, diff --git a/src/py_alpaca_api/stock/assets.py b/src/py_alpaca_api/stock/assets.py index 9da4896..2b07553 100644 --- a/src/py_alpaca_api/stock/assets.py +++ b/src/py_alpaca_api/stock/assets.py @@ -1,14 +1,14 @@ import json -from typing import Dict, List import pandas as pd +from py_alpaca_api.exceptions import APIRequestError from py_alpaca_api.http.requests import Requests from py_alpaca_api.models.asset_model import AssetModel, asset_class_from_dict class Assets: - def __init__(self, base_url: str, headers: Dict[str, str]) -> None: + def __init__(self, base_url: str, headers: dict[str, str]) -> None: self.base_url = base_url self.headers = headers @@ -16,8 +16,7 @@ def __init__(self, base_url: str, headers: Dict[str, str]) -> None: # Get Asset ############################################ def get(self, symbol: str) -> AssetModel: - """ - Retrieves an AssetModel for the specified symbol. + """Retrieves an AssetModel for the specified symbol. Args: symbol (str): The symbol of the asset to retrieve. @@ -28,12 +27,19 @@ def get(self, symbol: str) -> AssetModel: Raises: Exception: If the asset is not a US Equity (stock). """ - url = f"{self.base_url}/assets/{symbol}" - response = json.loads(Requests().request("GET", url, headers=self.headers).text) + http_response = Requests().request("GET", url, headers=self.headers) + + if http_response.status_code != 200: + raise APIRequestError( + http_response.status_code, + f"Failed to retrieve asset: {http_response.status_code}", + ) + + response = json.loads(http_response.text) if response.get("class") != "us_equity": - raise Exception("Asset is not a US Equity (stock)") + raise APIRequestError(400, "Asset is not a US Equity (stock)") return asset_class_from_dict(response) @@ -44,39 +50,47 @@ def get_all( self, status: str = "active", exchange: str = "", - excluded_exchanges: List[str] = ["OTC"], + excluded_exchanges: list[str] | None = None, ) -> pd.DataFrame: - """ - Retrieves a DataFrame of all active, fractionable, and tradable US equity assets, excluding those from the - OTC exchange. + """Retrieves a DataFrame of all active, fractionable, and tradable assets. + + Excluding those from the OTC exchange. Args: - status (str, optional): The status of the assets to retrieve. Defaults to "active". - exchange (str, optional): The exchange to filter the assets by. Defaults to an empty string, - which retrieves assets from all exchanges. - excluded_exchanges (List[str], optional): A list of exchanges to exclude from the results. - Defaults to ["OTC"]. + status (str, optional): The status of the assets to retrieve. + Defaults to "active". + exchange (str, optional): The exchange to filter the assets by. + Defaults to an empty string, which retrieves assets from + all exchanges. + excluded_exchanges (List[str], optional): A list of exchanges to + exclude from the results. Defaults to ["OTC"]. Returns: pd.DataFrame: A DataFrame containing the retrieved assets. """ - + if excluded_exchanges is None: + excluded_exchanges = ["OTC"] url = f"{self.base_url}/assets" - params = {"status": status, "asset_class": "us_equity", "exchange": exchange} + params: dict[str, str | bool | float | int] = { + "status": status, + "asset_class": "us_equity", + "exchange": exchange, + } response = json.loads( Requests().request("GET", url, headers=self.headers, params=params).text ) assets_df = pd.DataFrame(response) - assets_df = assets_df[ + filtered_df = assets_df[ (assets_df["status"] == "active") & assets_df["fractionable"] & assets_df["tradable"] & ~assets_df["exchange"].isin(excluded_exchanges) ].reset_index(drop=True) - assets_df = assets_df.astype( + assert isinstance(filtered_df, pd.DataFrame) + result_df = filtered_df.astype( { "id": "string", "class": "string", @@ -92,5 +106,4 @@ def get_all( "maintenance_margin_requirement": "float", } ) - - return assets_df + return result_df diff --git a/src/py_alpaca_api/stock/history.py b/src/py_alpaca_api/stock/history.py index 8b75ca0..787ad90 100644 --- a/src/py_alpaca_api/stock/history.py +++ b/src/py_alpaca_api/stock/history.py @@ -1,6 +1,5 @@ import json from collections import defaultdict -from typing import Dict import pandas as pd @@ -10,9 +9,8 @@ class History: - def __init__(self, data_url: str, headers: Dict[str, str], asset: Assets) -> None: - """ - Initializes an instance of the History class. + def __init__(self, data_url: str, headers: dict[str, str], asset: Assets) -> None: + """Initializes an instance of the History class. Args: data_url: A string representing the URL of the data. @@ -41,7 +39,7 @@ def check_if_stock(self, symbol: str) -> AssetModel: try: asset = self.asset.get(symbol) except Exception as e: - raise ValueError(str(e)) + raise ValueError(str(e)) from e if asset.asset_class != "us_equity": raise ValueError(f"{symbol} is not a stock.") @@ -63,8 +61,7 @@ def get_stock_data( sort: str = "asc", adjustment: str = "raw", ) -> pd.DataFrame: - """ - Retrieves historical stock data for a given symbol within a specified date range and timeframe. + """Retrieves historical stock data for a given symbol within a specified date range and timeframe. Args: symbol: The stock symbol to fetch data for. @@ -115,8 +112,7 @@ def get_stock_data( "sort": sort, } symbol_data = self.get_historical_data(symbol, url, params) - bar_data_df = self.preprocess_data(symbol_data, symbol) - return bar_data_df + return self.preprocess_data(symbol_data, symbol) ########################################### # /////////// PreProcess Data \\\\\\\\\\\ # @@ -134,7 +130,6 @@ def preprocess_data(symbol_data: list[defaultdict], symbol: str) -> pd.DataFrame Returns: A pandas DataFrame containing the preprocessed historical stock data. """ - bar_data_df = pd.DataFrame(symbol_data) bar_data_df.insert(0, "symbol", symbol) @@ -156,7 +151,7 @@ def preprocess_data(symbol_data: list[defaultdict], symbol: str) -> pd.DataFrame inplace=True, ) - bar_data_df = bar_data_df.astype( + return bar_data_df.astype( { "open": "float", "high": "float", @@ -170,16 +165,13 @@ def preprocess_data(symbol_data: list[defaultdict], symbol: str) -> pd.DataFrame } ) - return bar_data_df - ########################################### # ///////// Get Historical Data \\\\\\\\\ # ########################################### def get_historical_data( self, symbol: str, url: str, params: dict ) -> list[defaultdict]: - """ - Retrieves historical data for a given symbol. + """Retrieves historical data for a given symbol. Args: symbol (str): The symbol for which to retrieve historical data. diff --git a/src/py_alpaca_api/stock/latest_quote.py b/src/py_alpaca_api/stock/latest_quote.py index 59f09a7..7751e38 100644 --- a/src/py_alpaca_api/stock/latest_quote.py +++ b/src/py_alpaca_api/stock/latest_quote.py @@ -1,20 +1,19 @@ import json -from typing import Dict, List, Optional from py_alpaca_api.http.requests import Requests -from py_alpaca_api.models.quote_model import quote_class_from_dict +from py_alpaca_api.models.quote_model import QuoteModel, quote_class_from_dict class LatestQuote: - def __init__(self, headers: Dict[str, str]) -> None: + def __init__(self, headers: dict[str, str]) -> None: self.headers = headers def get( self, - symbol: Optional[List[str] | str], + symbol: list[str] | str | None, feed: str = "iex", currency: str = "USD", - ) -> dict: + ) -> list[QuoteModel] | QuoteModel: if symbol is None or symbol == "": raise ValueError("Symbol is required. Must be a string or list of strings.") @@ -29,7 +28,11 @@ def get( url = "https://data.alpaca.markets/v2/stocks/quotes/latest" - params = {"symbols": symbol, "feed": feed, "currency": currency} + params: dict[str, str | bool | float | int] = { + "symbols": symbol, + "feed": feed, + "currency": currency, + } response = json.loads( Requests() diff --git a/src/py_alpaca_api/stock/predictor.py b/src/py_alpaca_api/stock/predictor.py index 2fd15b6..0ef5618 100644 --- a/src/py_alpaca_api/stock/predictor.py +++ b/src/py_alpaca_api/stock/predictor.py @@ -2,12 +2,10 @@ import pandas as pd import pendulum -import numpy as np -np.float_ = np.float64 -from prophet import Prophet # noqa: E402 +from prophet import Prophet -from py_alpaca_api.stock.history import History # noqa: E402 -from py_alpaca_api.stock.screener import Screener # noqa: E402 +from py_alpaca_api.stock.history import History +from py_alpaca_api.stock.screener import Screener yesterday = pendulum.now().subtract(days=1).format("YYYY-MM-DD") four_years_ago = pendulum.now().subtract(years=2).format("YYYY-MM-DD") @@ -29,8 +27,7 @@ def get_stock_data( start: str = four_years_ago, end: str = yesterday, ) -> pd.DataFrame: - """ - Retrieves historical stock data for a given symbol within a specified timeframe. + """Retrieves historical stock data for a given symbol within a specified timeframe. Args: symbol (str): The stock symbol to retrieve data for. @@ -49,12 +46,13 @@ def get_stock_data( ) stock_df.rename(columns={"date": "ds", "vwap": "y"}, inplace=True) - return stock_df[["ds", "y"]] + result = stock_df[["ds", "y"]].copy() + assert isinstance(result, pd.DataFrame) + return result @staticmethod def train_prophet_model(data): - """ - Trains a Prophet model using the provided data. + """Trains a Prophet model using the provided data. Args: data: The input data used for training the model. @@ -66,9 +64,9 @@ def train_prophet_model(data): changepoint_prior_scale=0.05, holidays_prior_scale=15, seasonality_prior_scale=10, - weekly_seasonality=True, - yearly_seasonality=True, - daily_seasonality=False, + weekly_seasonality="auto", + yearly_seasonality="auto", + daily_seasonality="auto", ) model.add_country_holidays(country_name="US") model.fit(data) @@ -76,8 +74,7 @@ def train_prophet_model(data): @staticmethod def generate_forecast(model, future_periods=14): - """ - Generates a forecast using the specified model for a given number of future periods. + """Generates a forecast using the specified model for a given number of future periods. Args: model: The model used for forecasting. @@ -133,7 +130,7 @@ def _handle_ticker(self, ticker, gain_ratio, future_periods, previous_day_losers gain_prediction = self._get_gain_prediction(symbol_forecast, previous_price) if gain_prediction >= gain_ratio: return True, ticker - except Exception as e: - logger.error(f"Error predicting {ticker}: {e}") + except Exception: + logger.exception(f"Error predicting {ticker}") return False, None diff --git a/src/py_alpaca_api/stock/screener.py b/src/py_alpaca_api/stock/screener.py index 30f357a..a95e7ef 100644 --- a/src/py_alpaca_api/stock/screener.py +++ b/src/py_alpaca_api/stock/screener.py @@ -1,24 +1,24 @@ -from collections import defaultdict import json -from typing import Dict +from collections import defaultdict +from collections.abc import Callable import pandas as pd import pendulum from py_alpaca_api.http.requests import Requests -from py_alpaca_api.trading.market import Market from py_alpaca_api.stock.assets import Assets +from py_alpaca_api.trading.market import Market class Screener: def __init__( self, data_url: str, - headers: Dict[str, str], + headers: dict[str, str], asset: Assets, market: Market, ) -> None: - """Initialize Screener class3 + """Initialize Screener class3. Parameters: ___________ @@ -52,18 +52,17 @@ def __init__( def filter_stocks( self, price_greater_than: float, - change_condition: callable, + change_condition: Callable[[pd.DataFrame], pd.Series], volume_greater_than: int, trade_count_greater_than: int, total_returned: int, ascending_order: bool, ) -> pd.DataFrame: - """ - Filter stocks based on given parameters. + """Filter stocks based on given parameters. Args: price_greater_than: The minimum price threshold for the stocks. - change_condition: A callable function that takes in a DataFrame and returns a boolean value. + change_condition: A callable function that takes in a DataFrame and returns a boolean Series. This function is used to filter the stocks based on a specific change condition. volume_greater_than: The minimum volume threshold for the stocks. trade_count_greater_than: The minimum trade count threshold for the stocks. @@ -75,10 +74,23 @@ def filter_stocks( """ self.set_dates() df = self._get_percentages(start=self.day_before_yesterday, end=self.yesterday) - df = df[df["price"] > price_greater_than] - df = df[change_condition(df)] - df = df[df["volume"] > volume_greater_than] - df = df[df["trades"] > trade_count_greater_than] + + # Apply filters step by step, ensuring DataFrame type is preserved + price_filter = df["price"] > price_greater_than + df = df.loc[price_filter].copy() + + # Apply the change condition (returns a boolean Series) + change_filter = change_condition(df) + df = df.loc[change_filter].copy() + + volume_filter = df["volume"] > volume_greater_than + df = df.loc[volume_filter].copy() + + trades_filter = df["trades"] > trade_count_greater_than + df = df.loc[trades_filter].copy() + + # Ensure df is a DataFrame before sorting + assert isinstance(df, pd.DataFrame) return ( df.sort_values(by="change", ascending=ascending_order) .reset_index(drop=True) @@ -96,8 +108,7 @@ def losers( trade_count_greater_than: int = 2000, total_losers_returned: int = 100, ) -> pd.DataFrame: - """ - Returns a filtered DataFrame of stocks that meet the specified conditions for losers. + """Returns a filtered DataFrame of stocks that meet the specified conditions for losers. Args: price_greater_than (float): The minimum price threshold for stocks to be considered losers. Default is 5.0. @@ -111,7 +122,6 @@ def losers( Returns: pd.DataFrame: A filtered DataFrame containing stocks that meet the specified conditions for losers. """ - return self.filter_stocks( price_greater_than, lambda df: df["change"] < change_less_than, @@ -132,8 +142,7 @@ def gainers( trade_count_greater_than: int = 2000, total_gainers_returned: int = 100, ) -> pd.DataFrame: - """ - Args: + """Args: price_greater_than (float): The minimum price threshold for the stocks to be included in the gainers list. Default is 5.0. change_greater_than (float): The minimum change (in percentage) threshold for the stocks to be included in @@ -167,8 +176,7 @@ def _get_percentages( end: str, timeframe: str = "1Day", ) -> pd.DataFrame: - """ - Retrieves stock data for a set of symbols and calculates the percentage change, price, volume, and trade count for each symbol. + """Retrieves stock data for a set of symbols and calculates the percentage change, price, volume, and trade count for each symbol. Args: start (str): The start date for the data retrieval, in the format "YYYY-MM-DD". @@ -178,10 +186,9 @@ def _get_percentages( Returns: pd.DataFrame: A Pandas DataFrame containing the calculated data for each symbol, including the symbol, percentage change, price, volume, and trade count. """ - url = f"{self.data_url}/stocks/bars" - params = { + params: dict[str, str | bool | float | int] = { "symbols": ",".join(self.asset.get_all()["symbol"].tolist()), "limit": 10000, "timeframe": timeframe, @@ -197,7 +204,7 @@ def _get_percentages( symbols_data = defaultdict(list) while True: - params["page_token"] = page_token + params["page_token"] = page_token or "" response = json.loads( Requests() .request(method="GET", url=url, headers=self.headers, params=params) @@ -240,8 +247,7 @@ def _get_percentages( # ///////////////// Set Dates \\\\\\\\\\\\\\\\\\ # ################################################## def set_dates(self): - """ - Sets the dates for the screener. + """Sets the dates for the screener. This method retrieves the last two trading dates from the market calendar and assigns them to the `yesterday` and `day_before_yesterday` attributes. diff --git a/src/py_alpaca_api/trading/__init__.py b/src/py_alpaca_api/trading/__init__.py index 4b31f58..709aaeb 100644 --- a/src/py_alpaca_api/trading/__init__.py +++ b/src/py_alpaca_api/trading/__init__.py @@ -1,11 +1,8 @@ -from typing import Dict +from py_alpaca_api.trading.account import Account from py_alpaca_api.trading.market import Market from py_alpaca_api.trading.news import News -from py_alpaca_api.trading.positions import Positions - -# from py_alpaca_api.trading.stock import Stock -from py_alpaca_api.trading.account import Account from py_alpaca_api.trading.orders import Orders +from py_alpaca_api.trading.positions import Positions from py_alpaca_api.trading.recommendations import Recommendations from py_alpaca_api.trading.watchlists import Watchlist @@ -24,7 +21,7 @@ def __init__(self, api_key: str, api_secret: str, api_paper: bool) -> None: ) self._initialize_components(headers=headers, base_url=base_url) - def _initialize_components(self, headers: Dict[str, str], base_url: str): + def _initialize_components(self, headers: dict[str, str], base_url: str): self.account = Account(headers=headers, base_url=base_url) self.market = Market(headers=headers, base_url=base_url) self.positions = Positions( diff --git a/src/py_alpaca_api/trading/account.py b/src/py_alpaca_api/trading/account.py index 59616fb..b03033f 100644 --- a/src/py_alpaca_api/trading/account.py +++ b/src/py_alpaca_api/trading/account.py @@ -1,6 +1,8 @@ -from typing import Dict, List import json + import pandas as pd + +from py_alpaca_api.exceptions import APIRequestError from py_alpaca_api.http.requests import Requests from py_alpaca_api.models.account_activity_model import ( AccountActivityModel, @@ -10,7 +12,7 @@ class Account: - def __init__(self, headers: Dict[str, str], base_url: str) -> None: + def __init__(self, headers: dict[str, str], base_url: str) -> None: self.headers = headers self.base_url = base_url @@ -18,50 +20,65 @@ def __init__(self, headers: Dict[str, str], base_url: str) -> None: # Get Account ############################################ def get(self) -> AccountModel: - """ - Retrieves the user's account information. + """Retrieves the user's account information. Returns: AccountModel: The user's account model. """ url = f"{self.base_url}/account" - response = json.loads(Requests().request("GET", url, headers=self.headers).text) + http_response = Requests().request("GET", url, headers=self.headers) + + if http_response.status_code != 200: + raise APIRequestError( + http_response.status_code, + f"Failed to retrieve account: {http_response.status_code}", + ) + + response = json.loads(http_response.text) return account_class_from_dict(response) ####################################### # Get Account Activities ####################################### def activities( - self, activity_type: str, date: str = None, until_date: str = None - ) -> List[AccountActivityModel]: - """ - Retrieves the account activities for the specified activity type, optionally filtered by date or until date. + self, + activity_type: str, + date: str | None = None, + until_date: str | None = None, + ) -> list[AccountActivityModel]: + """Retrieves the account activities for the specified activity type. + + Optionally filtered by date or until date. Args: activity_type (str): The type of account activity to retrieve. - date (str, optional): The date to filter the activities by. If provided, only activities on this date will be returned. - until_date (str, optional): The date to filter the activities up to. If provided, only activities up to and including this date will be returned. + date (str, optional): The date to filter the activities by. + If provided, only activities on this date will be returned. + until_date (str, optional): The date to filter the activities up to. + If provided, only activities up to and including this date + will be returned. Returns: - List[AccountActivityModel]: A list of account activity models representing the retrieved activities. + List[AccountActivityModel]: A list of account activity models + representing the retrieved activities. Raises: - ValueError: If the activity type is not provided, or if both date and until_date are provided. + ValueError: If the activity type is not provided, or if both + date and until_date are provided. """ if not activity_type: - raise ValueError("Activity type is required.") + raise ValueError() if date and until_date: - raise ValueError( - "One or none of the Date and Until Date are required, not both." - ) + raise ValueError() url = f"{self.base_url}/account/activities/{activity_type}" - params = { - "date": date if date else None, - "until_date": until_date if until_date else None, - } + params: dict[str, str | bool | float | int] = {} + if date: + params["date"] = date + if until_date: + params["until_date"] = until_date response = json.loads( Requests() @@ -80,11 +97,15 @@ def portfolio_history( timeframe: str = "1D", intraday_reporting: str = "market_hours", ) -> pd.DataFrame: - """ + """Retrieves portfolio history data. + Args: - period (str): The period of time for which the portfolio history is requested. Defaults to "1W" (1 week). - timeframe (str): The timeframe for the intervals of the portfolio history. Defaults to "1D" (1 day). - intraday_reporting (str): The type of intraday reporting to be used. Defaults to "market_hours". + period (str): The period of time for which the portfolio history + is requested. Defaults to "1W" (1 week). + timeframe (str): The timeframe for the intervals of the portfolio + history. Defaults to "1D" (1 day). + intraday_reporting (str): The type of intraday reporting to be used. + Defaults to "market_hours". Returns: pd.DataFrame: A pandas DataFrame containing the portfolio history data. @@ -92,10 +113,9 @@ def portfolio_history( Raises: Exception: If the request to the Alpaca API fails. """ - url = f"{self.base_url}/account/portfolio/history" - params = { + params: dict[str, str | bool | float | int] = { "period": period, "timeframe": timeframe, "intraday_reporting": intraday_reporting, @@ -107,31 +127,59 @@ def portfolio_history( .text ) - portfolio_df = pd.DataFrame( - response, - columns=[ + if not response or not any(response.values()): + return pd.DataFrame() + + portfolio_df = pd.DataFrame(response) + + # Only set columns if we have data + if not portfolio_df.empty: + # The API may return different numbers of columns depending on the account type + # We only rename the columns we expect + expected_columns = [ "timestamp", "equity", "profit_loss", "profit_loss_pct", "base_value", - ], - ) + ] + + # Only rename columns if we have the expected number or more + if len(portfolio_df.columns) >= len(expected_columns): + # Rename the first 5 columns + portfolio_df.columns = pd.Index( + expected_columns + + list(portfolio_df.columns[len(expected_columns) :]) + ) + # Keep only the expected columns + portfolio_df = portfolio_df[expected_columns] + else: + # If we have fewer columns than expected, just rename what we have + portfolio_df.columns = pd.Index( + expected_columns[: len(portfolio_df.columns)] + ) + + # Convert timestamp column - explicitly handle as Series + timestamp_series: pd.Series = pd.Series( + pd.to_datetime(portfolio_df["timestamp"], unit="s") + ) + # Now we can safely use dt accessor on the Series + timestamp_transformed = ( + timestamp_series.dt.tz_localize("America/New_York") + .dt.tz_convert("UTC") + .dt.date + ) + portfolio_df["timestamp"] = timestamp_transformed + portfolio_df = portfolio_df.astype( + { + "equity": "float", + "profit_loss": "float", + "profit_loss_pct": "float", + "base_value": "float", + } + ) + portfolio_df["profit_loss_pct"] = portfolio_df["profit_loss_pct"] * 100 - timestamp_transformed = ( - pd.to_datetime(portfolio_df["timestamp"], unit="s") - .dt.tz_localize("America/New_York") - .dt.tz_convert("UTC") - .apply(lambda x: x.date()) - ) - portfolio_df["timestamp"] = timestamp_transformed - portfolio_df = portfolio_df.astype( - { - "equity": "float", - "profit_loss": "float", - "profit_loss_pct": "float", - "base_value": "float", - } - ) - portfolio_df["profit_loss_pct"] = portfolio_df["profit_loss_pct"] * 100 + # Ensure we always return a DataFrame + assert isinstance(portfolio_df, pd.DataFrame) return portfolio_df diff --git a/src/py_alpaca_api/trading/market.py b/src/py_alpaca_api/trading/market.py index 98d662d..b04567c 100644 --- a/src/py_alpaca_api/trading/market.py +++ b/src/py_alpaca_api/trading/market.py @@ -1,5 +1,4 @@ import json -from typing import Dict import pandas as pd @@ -8,13 +7,12 @@ class Market: - def __init__(self, base_url: str, headers: Dict[str, str]) -> None: + def __init__(self, base_url: str, headers: dict[str, str]) -> None: self.base_url = base_url self.headers = headers def clock(self) -> ClockModel: - """ - Retrieves the current market clock. + """Retrieves the current market clock. Returns: ClockModel: A model containing the current market clock data. @@ -28,8 +26,7 @@ def clock(self) -> ClockModel: return clock_class_from_dict(response) def calendar(self, start_date: str, end_date: str) -> pd.DataFrame: - """ - Retrieves the market calendar for the specified date range. + """Retrieves the market calendar for the specified date range. Args: start_date (str): The start date of the calendar range in the format "YYYY-MM-DD". @@ -38,9 +35,11 @@ def calendar(self, start_date: str, end_date: str) -> pd.DataFrame: Returns: pd.DataFrame: A DataFrame containing the market calendar data, with columns for the date, settlement date, open time, and close time. """ - url = f"{self.base_url}/calendar" - params = {"start": start_date, "end": end_date} + params: dict[str, str | bool | float | int] = { + "start": start_date, + "end": end_date, + } response = json.loads( Requests() .request(method="GET", url=url, headers=self.headers, params=params) diff --git a/src/py_alpaca_api/trading/news.py b/src/py_alpaca_api/trading/news.py index a500a42..d3f72ca 100644 --- a/src/py_alpaca_api/trading/news.py +++ b/src/py_alpaca_api/trading/news.py @@ -1,21 +1,21 @@ import json import logging +import math import textwrap import time -import math -from typing import Dict, List + import pendulum -from bs4 import BeautifulSoup as bs import yfinance as yf -from py_alpaca_api.http.requests import Requests - - +from bs4 import BeautifulSoup +from py_alpaca_api.http.requests import Requests +logger = logging.getLogger(__name__) -logger = logging.getLogger("yfinance") -logger.disabled = True -logger.propagate = False +# Disable yfinance logging +yfinance_logger = logging.getLogger("yfinance") +yfinance_logger.disabled = True +yfinance_logger.propagate = False START_DATE = pendulum.now().subtract(days=14).to_date_string() @@ -23,14 +23,13 @@ class News: - def __init__(self, headers: Dict[str, str]) -> None: + def __init__(self, headers: dict[str, str]) -> None: self.news_url = "https://data.alpaca.markets/v1beta1/news" self.headers = headers @staticmethod def strip_html(content: str): - """ - Removes HTML tags and returns the stripped content. + """Removes HTML tags and returns the stripped content. Args: content (str): The HTML content to be stripped. @@ -38,21 +37,32 @@ def strip_html(content: str): Returns: str: The stripped content without HTML tags. """ - soup = bs(content, "html.parser") + soup = BeautifulSoup(content, "html.parser") for data in soup(["style", "script"]): data.decompose() return " ".join(soup.stripped_strings) @staticmethod - def scrape_article(url: str) -> str: - """ - Scrapes the article text from the given URL. + def _parse_date_safe(date_str: str) -> str: + """Safely parse a date string with pendulum.""" + try: + parsed = pendulum.parse(date_str) + if isinstance(parsed, pendulum.DateTime): + return parsed.to_datetime_string() + # If not a DateTime, convert to string and return + return str(parsed) + except Exception: + return date_str + + @staticmethod + def scrape_article(url: str) -> str | None: + """Scrapes the article text from the given URL. Args: url (str): The URL of the article. Returns: - str: The text content of the article, or None if the article body is not found. + str | None: The text content of the article, or None if the article body is not found. """ time.sleep(1) # Sleep for 1 second to avoid rate limiting headers = { @@ -64,20 +74,16 @@ def scrape_article(url: str) -> str: like Gecko) Chrome/85.0.4183.83 Safari/537.36 Edg/85.0.564.44", } request = Requests().request(method="GET", url=url, headers=headers) - soup = bs(request.text, "html.parser") - return ( - soup.find(class_="caas-body").text - if soup.find(class_="caas-body") - else None - ) + soup = BeautifulSoup(request.text, "html.parser") + caas_body = soup.find(class_="caas-body") + return caas_body.text if caas_body is not None else None ######################################################## # //////////// static _truncate method //////////////# ######################################################## @staticmethod def truncate(text: str, length: int) -> str: - """ - Truncates a given text to a specified length. + """Truncates a given text to a specified length. Args: text (str): The text to be truncated. @@ -92,12 +98,11 @@ def truncate(text: str, length: int) -> str: else text ) - def get_news(self, symbol: str, limit: int = 6) -> List[Dict[str, str]]: - """ - Retrieves news articles related to a given symbol from Benzinga and Yahoo Finance. + def get_news(self, symbol: str, limit: int = 6) -> list[dict[str, str]]: + """Retrieves news articles related to a given symbol from Benzinga and Yahoo Finance. - Note: Yahoo Finance has implemented anti-scraping measures that prevent fetching - full article content. Yahoo news will include title, URL, publish date, and + Note: Yahoo Finance has implemented anti-scraping measures that prevent fetching + full article content. Yahoo news will include title, URL, publish date, and summary/description when available, but not full article text. Args: @@ -109,7 +114,9 @@ def get_news(self, symbol: str, limit: int = 6) -> List[Dict[str, str]]: """ benzinga_news = self._get_benzinga_news(symbol=symbol, limit=limit) yahoo_news = self._get_yahoo_news( - symbol=symbol, limit=(limit - len(benzinga_news[: (math.floor(limit / 2))])), scrape_content=False + symbol=symbol, + limit=(limit - len(benzinga_news[: (math.floor(limit / 2))])), + scrape_content=False, ) news = benzinga_news[: (math.floor(limit / 2))] + yahoo_news @@ -120,18 +127,19 @@ def get_news(self, symbol: str, limit: int = 6) -> List[Dict[str, str]]: return sorted_news[:limit] - def _get_yahoo_news(self, symbol: str, limit: int = 6, scrape_content: bool = False) -> List[Dict[str, str]]: - """ - Retrieves the latest news articles related to a given symbol from Yahoo Finance. + def _get_yahoo_news( + self, symbol: str, limit: int = 6, scrape_content: bool = False + ) -> list[dict[str, str]]: + """Retrieves the latest news articles related to a given symbol from Yahoo Finance. Args: symbol (str): The symbol for which to retrieve news articles. limit (int, optional): The maximum number of news articles to retrieve. Defaults to 6. - scrape_content (bool, optional): Whether to attempt scraping full article content. + scrape_content (bool, optional): Whether to attempt scraping full article content. Defaults to False due to Yahoo's anti-scraping measures. Returns: - list: A list of dictionaries containing the news article details, including title, URL, source, + list: A list of dictionaries containing the news article details, including title, URL, source, content (if available), publish date, and symbol. """ ticker = yf.Ticker(symbol) @@ -141,46 +149,61 @@ def _get_yahoo_news(self, symbol: str, limit: int = 6, scrape_content: bool = Fa news_count = 0 for news in news_response[:limit]: # Limit the iteration try: - news_content = news.get('content', {}) - + news_content = news.get("content", {}) + # Extract the summary/description if available content = None if scrape_content: # Only attempt scraping if explicitly requested try: - scraped_article = self.scrape_article(news_content.get("canonicalUrl", {}).get('url', '')) + scraped_article = self.scrape_article( + news_content.get("canonicalUrl", {}).get("url", "") + ) if scraped_article: - content = self.truncate(self.strip_html(scraped_article), 8000) + content = self.truncate( + self.strip_html(scraped_article), 8000 + ) except Exception as scrape_error: - logging.debug(f"Could not scrape article content: {scrape_error}") - + logger.debug( + f"Could not scrape article content: {scrape_error}" + ) + # Use the summary from the API if scraping failed or wasn't attempted if not content: # Try to get summary from the news data itself - summary = news_content.get('summary', '') + summary = news_content.get("summary", "") if not summary: # Some news items have description instead of summary - summary = news.get('summary', '') + summary = news.get("summary", "") content = self.truncate(summary, 8000) if summary else None - + yahoo_news.append( { - "title": news_content.get("title", news.get("title", "No title")), - "url": news_content.get("canonicalUrl", {}).get('url', news.get("link", "")), + "title": news_content.get( + "title", news.get("title", "No title") + ), + "url": news_content.get("canonicalUrl", {}).get( + "url", news.get("link", "") + ), "source": "yahoo", "content": content, "publish_date": pendulum.from_timestamp( - news_content.get("pubDate", news.get("providerPublishTime", 0)) - ).to_datetime_string() if news_content.get("pubDate") or news.get("providerPublishTime") else pendulum.now().to_datetime_string(), + news_content.get( + "pubDate", news.get("providerPublishTime", 0) + ) + ).to_datetime_string() + if news_content.get("pubDate") + or news.get("providerPublishTime") + else pendulum.now().to_datetime_string(), "symbol": symbol, } ) news_count += 1 - - except Exception as e: - logging.error(f"Error processing Yahoo news item: {e}") + + except Exception: + logger.exception("Error processing Yahoo news item") continue - + if news_count >= limit: break @@ -194,9 +217,8 @@ def _get_benzinga_news( include_content: bool = True, exclude_contentless: bool = True, limit: int = 10, - ) -> List[Dict[str, str]]: - """ - Retrieves Benzinga news articles for a given symbol and date range. + ) -> list[dict[str, str]]: + """Retrieves Benzinga news articles for a given symbol and date range. Args: symbol (str): The symbol for which to retrieve news articles. @@ -216,7 +238,7 @@ def _get_benzinga_news( - "symbol": The symbol associated with the news article. """ url = f"{self.news_url}" - params = { + params: dict[str, str | bool | float | int] = { "symbols": symbol, "start": start_date, "end": end_date, @@ -240,9 +262,7 @@ def _get_benzinga_news( "content": self.strip_html(news["content"]) if news["content"] else None, - "publish_date": pendulum.parse( - news["created_at"] - ).to_datetime_string(), + "publish_date": self._parse_date_safe(news["created_at"]), "symbol": symbol, } ) diff --git a/src/py_alpaca_api/trading/orders.py b/src/py_alpaca_api/trading/orders.py index 7fb8c72..8cf8d82 100644 --- a/src/py_alpaca_api/trading/orders.py +++ b/src/py_alpaca_api/trading/orders.py @@ -1,14 +1,13 @@ import json -from typing import Dict +from py_alpaca_api.exceptions import ValidationError from py_alpaca_api.http.requests import Requests from py_alpaca_api.models.order_model import OrderModel, order_class_from_dict class Orders: - def __init__(self, base_url: str, headers: Dict[str, str]) -> None: - """ - Initializes a new instance of the Order class. + def __init__(self, base_url: str, headers: dict[str, str]) -> None: + """Initializes a new instance of the Order class. Args: base_url (str): The URL for trading. @@ -24,12 +23,12 @@ def __init__(self, base_url: str, headers: Dict[str, str]) -> None: # \\\\\\\\\///////// Get Order BY id \\\\\\\///////////# ######################################################### def get_by_id(self, order_id: str, nested: bool = False) -> OrderModel: - """ - Retrieves order information by its ID. + """Retrieves order information by its ID. Args: order_id (str): The ID of the order to retrieve. - nested (bool, optional): Whether to include nested objects in the response. Defaults to False. + nested (bool, optional): Whether to include nested objects in the response. + Defaults to False. Returns: OrderModel: An object representing the order information. @@ -37,7 +36,7 @@ def get_by_id(self, order_id: str, nested: bool = False) -> OrderModel: Raises: ValueError: If the request to retrieve order information fails. """ - params = {"nested": nested} + params: dict[str, str | bool | float | int] = {"nested": nested} url = f"{self.base_url}/orders/{order_id}" response = json.loads( @@ -51,8 +50,7 @@ def get_by_id(self, order_id: str, nested: bool = False) -> OrderModel: # \\\\\\\\\\\\\\\\\ Cancel Order By ID /////////////////# ######################################################## def cancel_by_id(self, order_id: str) -> str: - """ - Cancel an order by its ID. + """Cancel an order by its ID. Args: order_id (str): The ID of the order to be cancelled. @@ -61,7 +59,8 @@ def cancel_by_id(self, order_id: str) -> str: str: A message indicating the status of the cancellation. Raises: - Exception: If the cancellation request fails, an exception is raised with the error message. + Exception: If the cancellation request fails, an exception is raised with the + error message. """ url = f"{self.base_url}/orders/{order_id}" @@ -73,14 +72,14 @@ def cancel_by_id(self, order_id: str) -> str: # \\\\\\\\\\\\\\\\ Cancel All Orders //////////////////# ######################################################## def cancel_all(self) -> str: - """ - Cancels all open orders. + """Cancels all open orders. Returns: str: A message indicating the number of orders that have been cancelled. Raises: - Exception: If the request to cancel orders is not successful, an exception is raised with the error message. + Exception: If the request to cancel orders is not successful, an exception is + raised with the error message. """ url = f"{self.base_url}/orders" @@ -92,19 +91,20 @@ def cancel_all(self) -> str: @staticmethod def check_for_order_errors( symbol: str, - qty: float = None, - notional: float = None, - take_profit: float = None, - stop_loss: float = None, + qty: float | None = None, + notional: float | None = None, + take_profit: float | None = None, + stop_loss: float | None = None, ) -> None: - """ - Checks for order errors based on the given parameters. + """Checks for order errors based on the given parameters. Args: symbol (str): The symbol for trading. qty (float, optional): The quantity of the order. Defaults to None. - notional (float, optional): The notional value of the order. Defaults to None. - take_profit (float, optional): The take profit value for the order. Defaults to None. + notional (float, optional): The notional value of the order. + Defaults to None. + take_profit (float, optional): The take profit value for the order. + Defaults to None. stop_loss (float, optional): The stop loss value for the order. Defaults to None. Raises: @@ -112,26 +112,28 @@ def check_for_order_errors( ValueError: If both qty and notional are provided or if neither is provided. ValueError: If either take_profit or stop_loss is not provided. ValueError: If both take_profit and stop_loss are not provided. - ValueError: If notional is provided or if qty is not an integer when both take_profit and + ValueError: If notional is provided or if qty is not an integer when both + take_profit and stop_loss are provided. Returns: None """ if not symbol: - raise ValueError("Must provide symbol for trading.") + raise ValueError() if not (qty or notional) or (qty and notional): - raise ValueError("Qty or Notional are required, not both.") + raise ValueError() - if take_profit and not stop_loss or stop_loss and not take_profit: - raise ValueError( - "Both take profit and stop loss are required for bracket orders." - ) + if (take_profit and not stop_loss) or (stop_loss and not take_profit): + raise ValueError() - if take_profit and stop_loss: - if notional or not qty.is_integer(): - raise ValueError("Bracket orders can not be fractionable.") + if ( + take_profit + and stop_loss + and (notional or (qty is not None and not qty.is_integer())) + ): + raise ValidationError() ######################################################## # \\\\\\\\\\\\\\\\ Submit Market Order ////////////////# @@ -139,28 +141,29 @@ def check_for_order_errors( def market( self, symbol: str, - qty: float = None, - notional: float = None, - take_profit: float = None, - stop_loss: float = None, + qty: float | None = None, + notional: float | None = None, + take_profit: float | None = None, + stop_loss: float | None = None, side: str = "buy", time_in_force: str = "day", extended_hours: bool = False, ) -> OrderModel: - """ - Submits a market order for a specified symbol. + """Submits a market order for a specified symbol. Args: symbol (str): The symbol of the asset to trade. - qty (float, optional): The quantity of the asset to trade. Either qty or notional must be provided, - but not both. Defaults to None. - notional (float, optional): The notional value of the asset to trade. Either qty or notional must be - provided, but not both. Defaults to None. + qty (float, optional): The quantity of the asset to trade. Either qty or notional + must be provided, but not both. Defaults to None. + notional (float, optional): The notional value of the asset to trade. + Either qty or notional must be provided, but not both. Defaults to None. take_profit (float, optional): The take profit price for the order. Defaults to None. stop_loss (float, optional): The stop loss price for the order. Defaults to None. side (str, optional): The side of the order (buy/sell). Defaults to "buy". - time_in_force (str, optional): The time in force for the order (day/gtc/opg/ioc/fok). Defaults to "day". - extended_hours (bool, optional): Whether to trade during extended hours. Defaults to False. + time_in_force (str, optional): The time in force for the order + (day/gtc/opg/ioc/fok). Defaults to "day". + extended_hours (bool, optional): Whether to trade during extended hours. + Defaults to False. Returns: OrderModel: An instance of the OrderModel representing the submitted order. @@ -173,13 +176,17 @@ def market( stop_loss=stop_loss, ) + # Convert take_profit and stop_loss floats to dicts before passing to\n # _submit_order + take_profit_dict = {"limit_price": take_profit} if take_profit else None + stop_loss_dict = {"stop_price": stop_loss} if stop_loss else None + return self._submit_order( symbol=symbol, side=side, qty=qty, notional=notional, - take_profit=take_profit, - stop_loss=stop_loss, + take_profit=take_profit_dict, + stop_loss=stop_loss_dict, entry_type="market", time_in_force=time_in_force, extended_hours=extended_hours, @@ -192,28 +199,33 @@ def limit( self, symbol: str, limit_price: float, - qty: float = None, - notional: float = None, - take_profit: float = None, - stop_loss: float = None, + qty: float | None = None, + notional: float | None = None, + take_profit: float | None = None, + stop_loss: float | None = None, side: str = "buy", time_in_force: str = "day", extended_hours: bool = False, ) -> OrderModel: - """ - Limit order function that submits an order to buy or sell a specified symbol at a specified limit price. + """Limit order function that submits an order to buy or sell a specified symbol + at a specified limit price. Args: symbol (str): The symbol of the asset to trade. limit_price (float): The limit price at which to execute the order. qty (float, optional): The quantity of the asset to trade. Default is None. - notional (float, optional): The amount of money to spend on the asset. Default is None. - take_profit (float, optional): The price at which to set a take profit order. Default is None. - stop_loss (float, optional): The price at which to set a stop loss order. Default is None. - side (str, optional): The side of the order. Must be either "buy" or "sell". Default is "buy". - time_in_force (str, optional): The duration of the order. Must be either "day" or "gtc" - (good till canceled). Default is "day". - extended_hours (bool, optional): Whether to allow trading during extended hours. Default is False. + notional (float, optional): The amount of money to spend on the asset. + Default is None. + take_profit (float, optional): The price at which to set a take profit order. + Default is None. + stop_loss (float, optional): The price at which to set a stop loss order. + Default is None. + side (str, optional): The side of the order. Must be either "buy" or "sell". + Default is "buy". + time_in_force (str, optional): The duration of the order. Must be either "day" + or "gtc" (good till canceled). Default is "day". + extended_hours (bool, optional): Whether to allow trading during extended + hours. Default is False. Returns: OrderModel: The submitted order. @@ -226,14 +238,18 @@ def limit( stop_loss=stop_loss, ) + # Convert take_profit and stop_loss floats to dicts before passing to\n # _submit_order + take_profit_dict = {"limit_price": take_profit} if take_profit else None + stop_loss_dict = {"stop_price": stop_loss} if stop_loss else None + return self._submit_order( symbol=symbol, side=side, limit_price=limit_price, qty=qty, notional=notional, - take_profit=take_profit, - stop_loss=stop_loss, + take_profit=take_profit_dict, + stop_loss=stop_loss_dict, entry_type="limit", time_in_force=time_in_force, extended_hours=extended_hours, @@ -248,22 +264,25 @@ def stop( stop_price: float, qty: float, side: str = "buy", - take_profit: float = None, - stop_loss: float = None, + take_profit: float | None = None, + stop_loss: float | None = None, time_in_force: str = "day", extended_hours: bool = False, ) -> OrderModel: - """ - Args: + """Args: + symbol: The symbol of the security to trade. stop_price: The stop price at which the trade should be triggered. qty: The quantity of shares to trade. side: The side of the trade. Defaults to 'buy'. - take_profit: The price at which to take profit on the trade. Defaults to None. - stop_loss: The price at which to set the stop loss on the trade. Defaults to None. - time_in_force: The duration for which the order will be in effect. Defaults to 'day'. - extended_hours: A boolean value indicating whether to place the order during extended hours. - Defaults to False. + take_profit: The price at which to take profit on the trade. + Defaults to None. + stop_loss: The price at which to set the stop loss on the trade. + Defaults to None. + time_in_force: The duration for which the order will be in effect. + Defaults to 'day'. + extended_hours: A boolean value indicating whether to place the order during + extended hours. Defaults to False. Returns: An instance of the OrderModel representing the submitted order. @@ -278,13 +297,17 @@ def stop( stop_loss=stop_loss, ) + # Convert take_profit and stop_loss floats to dicts before passing to\n # _submit_order + take_profit_dict = {"limit_price": take_profit} if take_profit else None + stop_loss_dict = {"stop_price": stop_loss} if stop_loss else None + return self._submit_order( symbol=symbol, side=side, stop_price=stop_price, qty=qty, - take_profit=take_profit, - stop_loss=stop_loss, + take_profit=take_profit_dict, + stop_loss=stop_loss_dict, entry_type="stop", time_in_force=time_in_force, extended_hours=extended_hours, @@ -303,17 +326,19 @@ def stop_limit( time_in_force: str = "day", extended_hours: bool = False, ) -> OrderModel: - """ - Submits a stop-limit order for trading. + """Submits a stop-limit order for trading. Args: symbol (str): The symbol of the security to trade. stop_price (float): The stop price for the order. limit_price (float): The limit price for the order. qty (float): The quantity of shares to trade. - side (str, optional): The side of the order, either 'buy' or 'sell'. Defaults to 'buy'. - time_in_force (str, optional): The time in force for the order. Defaults to 'day'. - extended_hours (bool, optional): Whether to allow trading during extended hours. Defaults to False. + side (str, optional): The side of the order, either 'buy' or 'sell'. + Defaults to 'buy'. + time_in_force (str, optional): The time in force for the order. + Defaults to 'day'. + extended_hours (bool, optional): Whether to allow trading during extended hours. + Defaults to False. Returns: OrderModel: The submitted stop-limit order. @@ -323,15 +348,14 @@ def stop_limit( ValueError: If neither limit_price nor stop_price is provided. ValueError: If qty is not provided. """ - if not symbol: - raise ValueError("Must provide symbol for trading.") + raise ValidationError() if not (limit_price or stop_price): - raise ValueError("Must provide limit and stop price for trading.") + raise ValidationError() if not qty: - raise ValueError("Qty is required.") + raise ValidationError() return self._submit_order( symbol=symbol, @@ -351,25 +375,24 @@ def trailing_stop( self, symbol: str, qty: float, - trail_percent: float = None, - trail_price: float = None, + trail_percent: float | None = None, + trail_price: float | None = None, side: str = "buy", time_in_force: str = "day", extended_hours: bool = False, ) -> OrderModel: - """ - Submits a trailing stop order for the specified symbol. + """Submits a trailing stop order for the specified symbol. Args: symbol (str): The symbol of the security to trade. qty (float): The quantity of shares to trade. - trail_percent (float, optional): The trailing stop percentage. Either `trail_percent` or `trail_price` - must be provided, not both. Defaults to None. - trail_price (float, optional): The trailing stop price. Either `trail_percent` or `trail_price` - must be provided, not both. Defaults to None. + trail_percent (float, optional): The trailing stop percentage. Either + `trail_percent` or `trail_price` must be provided, not both. Defaults to None. + trail_price (float, optional): The trailing stop price. Either + `trail_percent` or `trail_price` must be provided, not both. Defaults to None. side (str, optional): The side of the order, either 'buy' or 'sell'. Defaults to 'buy'. time_in_force (str, optional): The time in force for the order. Defaults to 'day'. - extended_hours (bool, optional): Whether to allow trading during extended hours. Defaults to False. + extended_hours (bool, optional): Whether to allow trading during extended hours.\n Defaults to False. Returns: OrderModel: The submitted trailing stop order. @@ -380,26 +403,19 @@ def trailing_stop( ValueError: If both `trail_percent` and `trail_price` are provided, or if neither is provided. ValueError: If `trail_percent` is less than 0. """ - if not symbol: - raise ValueError("Must provide symbol for trading.") + raise ValidationError() if not qty: - raise ValueError("Qty is required.") + raise ValidationError() - if ( - trail_percent is None - and trail_price is None - or trail_percent - and trail_price + if (trail_percent is None and trail_price is None) or ( + trail_percent and trail_price ): - raise ValueError( - "Either trail_percent or trail_price must be provided, not both." - ) + raise ValidationError() - if trail_percent: - if trail_percent < 0: - raise ValueError("Trail percent must be greater than 0.") + if trail_percent and trail_percent < 0: + raise ValidationError() return self._submit_order( symbol=symbol, @@ -419,35 +435,42 @@ def _submit_order( self, symbol: str, entry_type: str, - qty: float = None, - notional: float = None, - stop_price: float = None, - limit_price: float = None, - trail_percent: float = None, - trail_price: float = None, - take_profit: Dict[str, float] = None, - stop_loss: Dict[str, float] = None, + qty: float | None = None, + notional: float | None = None, + stop_price: float | None = None, + limit_price: float | None = None, + trail_percent: float | None = None, + trail_price: float | None = None, + take_profit: dict[str, float] | None = None, + stop_loss: dict[str, float] | None = None, side: str = "buy", time_in_force: str = "day", extended_hours: bool = False, ) -> OrderModel: - """ - Submits an order to the Alpaca API. + """Submits an order to the Alpaca API. Args: symbol (str): The symbol of the security to trade. entry_type (str): The type of order to submit. - qty (float, optional): The quantity of shares to trade. Defaults to None. - notional (float, optional): The notional value of the trade. Defaults to None. - stop_price (float, optional): The stop price for a stop order. Defaults to None. + qty (float, optional): The quantity of shares to trade. + Defaults to None. + notional (float, optional): The notional value of the trade. + Defaults to None. + stop_price (float, optional): The stop price for a stop order. + Defaults to None. limit_price (float, optional): The limit price for a limit order. Defaults to None. - trail_percent (float, optional): The trailing stop percentage for a trailing stop order. Defaults to None. - trail_price (float, optional): The trailing stop price for a trailing stop order. Defaults to None. - take_profit (Dict[str, float], optional): The take profit parameters for the order. Defaults to None. - stop_loss (Dict[str, float], optional): The stop loss parameters for the order. Defaults to None. + trail_percent (float, optional): The trailing stop percentage for a trailing + stop order. Defaults to None. + trail_price (float, optional): The trailing stop price for a trailing stop + order. Defaults to None. + take_profit (Dict[str, float], optional): The take profit parameters for the + order. Defaults to None. + stop_loss (Dict[str, float], optional): The stop loss parameters for the order. + Defaults to None. side (str, optional): The side of the trade (buy or sell). Defaults to "buy". - time_in_force (str, optional): The time in force for the order. Defaults to "day". - extended_hours (bool, optional): Whether to allow trading during extended hours. Defaults to False. + time_in_force (str, optional): The time in force for the order. + Defaults to "day". + extended_hours (bool, optional): Whether to allow trading during extended hours.\n Defaults to False. Returns: OrderModel: The submitted order. @@ -464,8 +487,8 @@ def _submit_order( "trail_percent": trail_percent if trail_percent else None, "trail_price": trail_price if trail_price else None, "order_class": "bracket" if take_profit or stop_loss else "simple", - "take_profit": ({"limit_price": take_profit} if take_profit else None), - "stop_loss": {"stop_price": stop_loss} if stop_loss else None, + "take_profit": take_profit, + "stop_loss": stop_loss, "side": side if side == "buy" else "sell", "type": entry_type, "time_in_force": time_in_force, diff --git a/src/py_alpaca_api/trading/positions.py b/src/py_alpaca_api/trading/positions.py index 21bf2ea..e6a1c67 100644 --- a/src/py_alpaca_api/trading/positions.py +++ b/src/py_alpaca_api/trading/positions.py @@ -1,5 +1,4 @@ import json -from typing import Dict import pandas as pd @@ -10,7 +9,7 @@ class Positions: def __init__( - self, base_url: str, headers: Dict[str, str], account: Account + self, base_url: str, headers: dict[str, str], account: Account ) -> None: self.base_url = base_url self.headers = headers @@ -20,8 +19,7 @@ def __init__( # \\\\\\\\\\\\\\\\ Close All Positions ////////////////# ######################################################## def close_all(self, cancel_orders: bool = False) -> str: - """ - Close all positions. + """Close all positions. Args: cancel_orders (bool, optional): Whether to cancel open orders associated with the positions. @@ -34,9 +32,8 @@ def close_all(self, cancel_orders: bool = False) -> str: Exception: If the request to close positions is not successful, an exception is raised with the error message from the API response. """ - url = f"{self.base_url}/positions" - params = {"cancel_orders": cancel_orders} + params: dict[str, str | bool | float | int] = {"cancel_orders": cancel_orders} response = json.loads( Requests() @@ -49,10 +46,9 @@ def close_all(self, cancel_orders: bool = False) -> str: # \\\\\\\\\\\\\\\\\\ Close Position ///////////////////# ######################################################## def close( - self, symbol_or_id: str, qty: float = None, percentage: int = None + self, symbol_or_id: str, qty: float | None = None, percentage: int | None = None ) -> str: - """ - Closes a position for a given symbol or asset ID. + """Closes a position for a given symbol or asset ID. Args: symbol_or_id (str): The symbol or asset ID of the position to be closed. @@ -69,7 +65,6 @@ def close( ValueError: If symbol_or_id is not provided. Exception: If the request to close the position fails. """ - if not qty and not percentage: raise ValueError("Quantity or percentage is required.") if qty and percentage: @@ -80,7 +75,12 @@ def close( raise ValueError("Symbol or asset_id is required.") url = f"{self.base_url}/positions/{symbol_or_id}" - params = {"qty": qty, "percentage": percentage} + # Filter out None values for params + params: dict[str, str | float | int] = {} + if qty is not None: + params["qty"] = qty + if percentage is not None: + params["percentage"] = percentage Requests().request( method="DELETE", url=url, headers=self.headers, params=params ) @@ -88,8 +88,7 @@ def close( return f"Position {symbol_or_id} has been closed" def get(self, symbol: str) -> PositionModel: - """ - Retrieves the position for the specified symbol. + """Retrieves the position for the specified symbol. Args: symbol (str): The symbol of the asset for which to retrieve the position. @@ -100,16 +99,17 @@ def get(self, symbol: str) -> PositionModel: Raises: ValueError: If the symbol is not provided or if a position for the specified symbol is not found. """ - if not symbol: raise ValueError("Symbol is required.") try: position = self.get_all().query(f"symbol == '{symbol}'").iloc[0] except IndexError: - raise ValueError(f"Position for symbol '{symbol}' not found.") + raise ValueError( + f"Position for symbol '{symbol}' not found." + ) from IndexError - return position_class_from_dict(position) + return position_class_from_dict(position.to_dict()) ############################################ # Get All Positions @@ -117,8 +117,7 @@ def get(self, symbol: str) -> PositionModel: def get_all( self, order_by: str = "profit_pct", order_asc: bool = False ) -> pd.DataFrame: - """ - Retrieves all positions for the user's Alpaca account, including cash positions. + """Retrieves all positions for the user's Alpaca account, including cash positions. The positions are returned as a pandas DataFrame, with the following columns: - asset_id: The unique identifier for the asset @@ -190,8 +189,7 @@ def get_all( ############################################ @staticmethod def modify_position_df(positions_df: pd.DataFrame) -> pd.DataFrame: - """ - Modifies the given positions DataFrame by renaming columns, converting data types, + """Modifies the given positions DataFrame by renaming columns, converting data types, and rounding values. Args: @@ -241,12 +239,8 @@ def modify_position_df(positions_df: pd.DataFrame) -> pd.DataFrame: round_2 = ["profit_dol", "intraday_profit_dol", "market_value"] round_4 = ["profit_pct", "intraday_profit_pct", "portfolio_pct"] - positions_df[round_2] = positions_df[round_2].apply( - lambda x: pd.Series.round(x, 2) - ) - positions_df[round_4] = positions_df[round_4].apply( - lambda x: pd.Series.round(x, 4) * 100 - ) + positions_df[round_2] = positions_df[round_2].apply(lambda x: x.round(2)) + positions_df[round_4] = positions_df[round_4].apply(lambda x: x.round(4) * 100) return positions_df @@ -254,32 +248,30 @@ def modify_position_df(positions_df: pd.DataFrame) -> pd.DataFrame: # Cash Position DataFrame ############################################ def cash_position_df(self): - """ - Retrieves the user's cash position data as a DataFrame. + """Retrieves the user's cash position data as a DataFrame. Returns: pd.DataFrame: A DataFrame containing the user's cash position data. """ return pd.DataFrame( - { - "asset_id": "", - "symbol": "Cash", - "exchange": "", - "asset_class": "", - "avg_entry_price": 0, - "qty": 0, - "qty_available": 0, - "side": "", - "market_value": self.account.get().cash, - "cost_basis": 0, - "unrealized_pl": 0, - "unrealized_plpc": 0, - "unrealized_intraday_pl": 0, - "unrealized_intraday_plpc": 0, - "current_price": 0, - "lastday_price": 0, - "change_today": 0, - "asset_marginable": False, - }, - index=[0], + data={ + "asset_id": [""], + "symbol": ["Cash"], + "exchange": [""], + "asset_class": [""], + "avg_entry_price": [0], + "qty": [0], + "qty_available": [0], + "side": [""], + "market_value": [self.account.get().cash], + "cost_basis": [0], + "unrealized_pl": [0], + "unrealized_plpc": [0], + "unrealized_intraday_pl": [0], + "unrealized_intraday_plpc": [0], + "current_price": [0], + "lastday_price": [0], + "change_today": [0], + "asset_marginable": [False], + } ) diff --git a/src/py_alpaca_api/trading/recommendations.py b/src/py_alpaca_api/trading/recommendations.py index ca62893..e4cd398 100644 --- a/src/py_alpaca_api/trading/recommendations.py +++ b/src/py_alpaca_api/trading/recommendations.py @@ -1,36 +1,40 @@ import time -from typing import Any, Dict, Union +from typing import Any + import pandas as pd import yfinance as yf - - - class Recommendations: def __init__(self) -> None: pass @staticmethod - def get_recommendations(symbol: str) -> Union[Dict[Any, Any], pd.DataFrame]: - """ - Retrieves the latest recommendations for a given stock symbol. + def get_recommendations(symbol: str) -> dict[Any, Any] | pd.DataFrame: + """Retrieves the latest recommendations for a given stock symbol. Args: symbol (str): The stock symbol for which to retrieve recommendations. Returns: - dict: A dictionary containing the latest recommendations for the stock symbol. + Union[dict, pd.DataFrame]: A dictionary or DataFrame containing the latest recommendations for the stock symbol. """ time.sleep(1) # To avoid hitting the API rate limit ticker = yf.Ticker(symbol) recommendations = ticker.recommendations - return recommendations.head(2) + # Handle the case where recommendations could be None or empty + if recommendations is None or not isinstance(recommendations, pd.DataFrame): + return {} + + # Ensure we return a DataFrame, not a Series + result = recommendations.head(2) + if isinstance(result, pd.Series): + return result.to_frame() + return result def get_sentiment(self, symbol: str) -> str: - """ - Retrieves the sentiment for a given stock symbol based on the latest recommendations. + """Retrieves the sentiment for a given stock symbol based on the latest recommendations. Args: symbol (str): The stock symbol for which to retrieve the sentiment. @@ -38,10 +42,16 @@ def get_sentiment(self, symbol: str) -> str: Returns: str: The sentiment for the stock symbol, either "BULLISH", "BEARISH", or "NEUTRAL". """ - recommendations = self.get_recommendations(symbol) - if recommendations.empty: + + # Type guard: check if recommendations is a DataFrame and not empty + if isinstance(recommendations, dict) or ( + isinstance(recommendations, pd.DataFrame) and recommendations.empty + ): return "NEUTRAL" + + # At this point we know recommendations is a non-empty DataFrame + assert isinstance(recommendations, pd.DataFrame) buy = recommendations["strongBuy"].sum() + recommendations["buy"].sum() sell = ( recommendations["strongSell"].sum() diff --git a/src/py_alpaca_api/trading/watchlists.py b/src/py_alpaca_api/trading/watchlists.py index 510fe9b..3b41149 100644 --- a/src/py_alpaca_api/trading/watchlists.py +++ b/src/py_alpaca_api/trading/watchlists.py @@ -1,6 +1,6 @@ import json -from typing import Dict, Union +from py_alpaca_api.exceptions import ValidationError from py_alpaca_api.http.requests import Requests from py_alpaca_api.models.watchlist_model import ( WatchlistModel, @@ -9,9 +9,8 @@ class Watchlist: - def __init__(self, base_url: str, headers: Dict[str, str]) -> None: - """ - Initialize a Watchlist object. + def __init__(self, base_url: str, headers: dict[str, str]) -> None: + """Initialize a Watchlist object. Args: base_url (str): The URL for trading. @@ -27,11 +26,8 @@ def __init__(self, base_url: str, headers: Dict[str, str]) -> None: # ///////////// Helper functions //////////////////////# ######################################################## @staticmethod - def _handle_response( - response: dict, no_content_msg: str - ) -> Union[WatchlistModel, str]: - """ - Handles the response from the API and returns a WatchlistModel object + def _handle_response(response: dict, no_content_msg: str) -> WatchlistModel | str: + """Handles the response from the API and returns a WatchlistModel object if the response is not empty, otherwise returns the specified no_content_msg. Args: @@ -43,17 +39,19 @@ def _handle_response( """ if response: return watchlist_class_from_dict(response) - else: - return no_content_msg + return no_content_msg ######################################################## # ///////////// Send a request to the API //////////////# ######################################################## def _request( - self, method: str, url: str, payload: dict = None, params: dict = None - ) -> Dict: - """ - Sends a request to the specified URL using the specified HTTP method. + self, + method: str, + url: str, + payload: dict | None = None, + params: dict | None = None, + ) -> dict: + """Sends a request to the specified URL using the specified HTTP method. Args: method (str): The HTTP method to use for the request (e.g., 'GET', 'POST', 'PUT', 'DELETE'). @@ -83,10 +81,9 @@ def _request( # //////////////// Get a watchlist ///////////////////# ######################################################## def get( - self, watchlist_id: str = None, watchlist_name: str = None - ) -> WatchlistModel: - """ - Retrieves a watchlist based on the provided watchlist ID or name. + self, watchlist_id: str | None = None, watchlist_name: str | None = None + ) -> WatchlistModel | str: + """Retrieves a watchlist based on the provided watchlist ID or name. Args: watchlist_id (str, optional): The ID of the watchlist to retrieve. @@ -99,7 +96,9 @@ def get( ValueError: If both watchlist_id and watchlist_name are provided, or if neither is provided. """ - if watchlist_id and watchlist_name or (not watchlist_id and not watchlist_name): + if (watchlist_id and watchlist_name) or ( + not watchlist_id and not watchlist_name + ): raise ValueError("Watchlist ID or Name is required, not both.") if watchlist_id: @@ -117,9 +116,8 @@ def get( ######################################################## # ///////////// Get all watchlists ////////////////////# ######################################################## - def get_all(self) -> list[WatchlistModel]: - """ - Retrieves all watchlists. + def get_all(self) -> list[WatchlistModel | str]: + """Retrieves all watchlists. Returns: A list of WatchlistModel objects representing all the watchlists. @@ -142,9 +140,10 @@ def get_all(self) -> list[WatchlistModel]: ######################################################## # ///////////// Create a new watchlist ////////////////# ######################################################## - def create(self, name: str, symbols: Union[list, str] = None) -> WatchlistModel: - """ - Creates a new watchlist with the given name and symbols. + def create( + self, name: str, symbols: list | str | None = None + ) -> WatchlistModel | str: + """Creates a new watchlist with the given name and symbols. Args: name (str): The name of the watchlist. @@ -174,13 +173,12 @@ def create(self, name: str, symbols: Union[list, str] = None) -> WatchlistModel: ######################################################## def update( self, - watchlist_id: str = None, - watchlist_name: str = None, + watchlist_id: str | None = None, + watchlist_name: str | None = None, name: str = "", - symbols: Union[list, str] = None, - ) -> WatchlistModel: - """ - Update a watchlist with the specified parameters. + symbols: list | str | None = None, + ) -> WatchlistModel | str: + """Update a watchlist with the specified parameters. Args: watchlist_id (str, optional): The ID of the watchlist to update. Either `watchlist_id` or `watchlist_name` @@ -200,8 +198,9 @@ def update( `watchlist_name` are provided. """ - - if watchlist_id and watchlist_name or (not watchlist_id and not watchlist_name): + if (watchlist_id and watchlist_name) or ( + not watchlist_id and not watchlist_name + ): raise ValueError("Watchlist ID or Name is required, not both.") # Check if watchlist_id is provided if watchlist_id: @@ -211,12 +210,16 @@ def update( watchlist = self.get(watchlist_name=watchlist_name) url = f"{self.base_url}/watchlists:by_name" + # Type guard to ensure watchlist is a WatchlistModel + if isinstance(watchlist, str): + raise TypeError(f"Failed to retrieve watchlist: {watchlist}") + name = name if name else watchlist.name if isinstance(symbols, str): symbols = symbols.replace(" ", "").split(",") elif isinstance(symbols, list): - symbols = symbols + pass else: symbols = ",".join([o.symbol for o in watchlist.assets]) @@ -231,9 +234,10 @@ def update( ######################################################## # ///////////// Delete a watchlist ////////////////////# ######################################################## - def delete(self, watchlist_id: str = None, watchlist_name: str = None) -> str: - """ - Deletes a watchlist. + def delete( + self, watchlist_id: str | None = None, watchlist_name: str | None = None + ) -> str: + """Deletes a watchlist. Args: watchlist_id (str, optional): The ID of the watchlist to delete. @@ -246,7 +250,9 @@ def delete(self, watchlist_id: str = None, watchlist_name: str = None) -> str: ValueError: If both watchlist_id and watchlist_name are provided or if neither is provided. """ - if watchlist_id and watchlist_name or (not watchlist_id and not watchlist_name): + if (watchlist_id and watchlist_name) or ( + not watchlist_id and not watchlist_name + ): raise ValueError("Watchlist ID or Name is required, not both.") if watchlist_id: @@ -257,22 +263,23 @@ def delete(self, watchlist_id: str = None, watchlist_name: str = None) -> str: params = {"name": watchlist_name} if watchlist_name else None response = self._request(method="DELETE", url=url, params=params) - return self._handle_response( + result = self._handle_response( response=response, no_content_msg=f"Watchlist {watchlist_id if watchlist_id else watchlist_name} deleted successfully.", ) + # Delete operations should return the success message string + return str(result) if isinstance(result, WatchlistModel) else result ######################################################## # ///////////// Add Asset to watchlist ///////////////# ######################################################## def add_asset( self, - watchlist_id: str = None, - watchlist_name: str = None, + watchlist_id: str | None = None, + watchlist_name: str | None = None, symbol: str = "", - ) -> WatchlistModel: - """ - Adds an asset to a watchlist. + ) -> WatchlistModel | str: + """Adds an asset to a watchlist. Args: watchlist_id (str): The ID of the watchlist to add the asset to. If `watchlist_id` is provided, @@ -289,7 +296,9 @@ def add_asset( ValueError: If `symbol` is not provided. """ - if watchlist_id and watchlist_name or (not watchlist_id and not watchlist_name): + if (watchlist_id and watchlist_name) or ( + not watchlist_id and not watchlist_name + ): raise ValueError("Watchlist ID or Name is required, not both.") if not symbol: @@ -314,12 +323,11 @@ def add_asset( ######################################################## def remove_asset( self, - watchlist_id: str = None, - watchlist_name: str = None, + watchlist_id: str | None = None, + watchlist_name: str | None = None, symbol: str = "", - ) -> WatchlistModel: - """ - Removes an asset from a watchlist. + ) -> WatchlistModel | str: + """Removes an asset from a watchlist. Args: watchlist_id (str, optional): The ID of the watchlist. If not provided, the watchlist_name parameter @@ -336,14 +344,19 @@ def remove_asset( Raises: ValueError: If both watchlist_id and watchlist_name are provided, or if symbol is not provided. """ - if watchlist_id and watchlist_name or (not watchlist_id and not watchlist_name): + if (watchlist_id and watchlist_name) or ( + not watchlist_id and not watchlist_name + ): raise ValueError("Watchlist ID or Name is required, not both.") if not symbol: raise ValueError("Symbol is required") if not watchlist_id: - watchlist_id = self.get(watchlist_name=watchlist_name).id + watchlist = self.get(watchlist_name=watchlist_name) + if isinstance(watchlist, str): + raise TypeError(f"Failed to retrieve watchlist: {watchlist}") + watchlist_id = watchlist.id url = f"{self.base_url}/watchlists/{watchlist_id}/{symbol}" @@ -356,9 +369,10 @@ def remove_asset( ######################################################## # /////////// Get Assets from a watchlist /////////////# ######################################################## - def get_assets(self, watchlist_id: str = None, watchlist_name: str = None) -> list: - """ - Retrieves the symbols of assets in a watchlist. + def get_assets( + self, watchlist_id: str | None = None, watchlist_name: str | None = None + ) -> list: + """Retrieves the symbols of assets in a watchlist. Args: watchlist_id (str, optional): The ID of the watchlist. Either `watchlist_id` or `watchlist_name` @@ -375,17 +389,18 @@ def get_assets(self, watchlist_id: str = None, watchlist_name: str = None) -> li ValueError: If both `watchlist_id` and `watchlist_name` are provided, or if neither `watchlist_id` nor `watchlist_name` are provided. """ - if watchlist_id and watchlist_name: - raise ValueError("Watchlist ID or Name is required, not both.") + raise ValidationError() if watchlist_id: watchlist = self.get(watchlist_id=watchlist_id) elif watchlist_name: watchlist = self.get(watchlist_name=watchlist_name) else: - raise ValueError("Watchlist ID or Name is required") + raise ValidationError() - symbols = [o.symbol for o in watchlist.assets] + # Type guard to ensure watchlist is a WatchlistModel + if isinstance(watchlist, str): + raise TypeError(f"Failed to retrieve watchlist: {watchlist}") - return symbols + return [o.symbol for o in watchlist.assets] diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..7185928 --- /dev/null +++ b/test.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Allow overriding via existing env; fallback to defaults below +export ALPACA_API_KEY="${ALPACA_API_KEY:-PK073RA92YGPPK42ZH44}" +export ALPACA_SECRET_KEY="${ALPACA_SECRET_KEY:-Wc9mvFugeICFlWhNVJJmvptMCNqU9JyNwPc72fzi}" + +# Activate local venv if present +if [ -d ".venv" ]; then + source .venv/bin/activate +fi + +# Run all tests by default; pass through targets/args when provided +if [ "$#" -eq 0 ]; then + uv run pytest -q tests +else + uv run pytest -q "$@" +fi diff --git a/tests/test_http/test_requests.py b/tests/test_http/test_requests.py index f93dbf4..ebf689b 100644 --- a/tests/test_http/test_requests.py +++ b/tests/test_http/test_requests.py @@ -1,5 +1,8 @@ -import pytest from unittest.mock import Mock, patch + +import pytest + +from py_alpaca_api.exceptions import APIRequestError from py_alpaca_api.http.requests import Requests @@ -21,9 +24,11 @@ def test_request_error(requests_obj): mock_response = Mock() mock_response.status_code = 400 mock_response.text = "Error" - with patch("requests.Session.request", return_value=mock_response): - with pytest.raises(Exception): - requests_obj.request("GET", "https://example.com") + with ( + patch("requests.Session.request", return_value=mock_response), + pytest.raises(APIRequestError), + ): + requests_obj.request("GET", "https://example.com") def test_request_with_headers(requests_obj): diff --git a/tests/test_models/test_account_activity_model.py b/tests/test_models/test_account_activity_model.py index 5e8e963..5e9f106 100644 --- a/tests/test_models/test_account_activity_model.py +++ b/tests/test_models/test_account_activity_model.py @@ -1,5 +1,6 @@ -import pytest import pendulum +import pytest + from py_alpaca_api.models.account_activity_model import ( AccountActivityModel, account_activity_class_from_dict, diff --git a/tests/test_models/test_account_model.py b/tests/test_models/test_account_model.py index 11906c8..83901e1 100644 --- a/tests/test_models/test_account_model.py +++ b/tests/test_models/test_account_model.py @@ -1,6 +1,7 @@ -from py_alpaca_api.models.account_model import AccountModel, account_class_from_dict import pendulum +from py_alpaca_api.models.account_model import AccountModel, account_class_from_dict + def test_account_class_from_dict(): data_dict = { diff --git a/tests/test_models/test_clock_model.py b/tests/test_models/test_clock_model.py index bb60cdc..f948941 100644 --- a/tests/test_models/test_clock_model.py +++ b/tests/test_models/test_clock_model.py @@ -1,6 +1,7 @@ -from py_alpaca_api.models.clock_model import ClockModel, clock_class_from_dict import pendulum +from py_alpaca_api.models.clock_model import ClockModel, clock_class_from_dict + def test_clock_class_from_dict(): data_dict = { diff --git a/tests/test_models/test_order_model.py b/tests/test_models/test_order_model.py index 97aaab5..3ced1b4 100644 --- a/tests/test_models/test_order_model.py +++ b/tests/test_models/test_order_model.py @@ -1,18 +1,28 @@ -import pytest -from py_alpaca_api.models.order_model import order_class_from_dict import pendulum +from py_alpaca_api.models.order_model import order_class_from_dict + class TestOrderClassFromDict: def test_order_class_from_dict_with_empty_dict(self): data_dict = {} - with pytest.raises(KeyError): - order_class_from_dict(data_dict) + # The function returns an OrderModel with default values for missing fields + order = order_class_from_dict(data_dict) + assert order.id == "" + assert order.symbol == "" + assert order.qty == 0.0 + assert order.status == "" + assert order.legs == [] def test_order_class_from_dict_with_missing_required_keys(self): data_dict = {"some_key": "some_value"} - with pytest.raises(KeyError): - order_class_from_dict(data_dict) + # The function returns an OrderModel with default values for missing fields + order = order_class_from_dict(data_dict) + assert order.id == "" + assert order.symbol == "" + assert order.qty == 0.0 + assert order.status == "" + assert order.legs == [] def test_order_class_from_dict_with_invalid_leg_data(self): data_dict = { @@ -21,8 +31,14 @@ def test_order_class_from_dict_with_invalid_leg_data(self): "asset_class": "equity", "legs": [{"invalid_key": "invalid_value"}], } - with pytest.raises(KeyError): - order_class_from_dict(data_dict) + # The function creates legs with default values for missing fields + order = order_class_from_dict(data_dict) + assert order.id == "order_123" + assert order.client_order_id == "client_order_123" + assert order.asset_class == "equity" + assert len(order.legs) == 1 + assert order.legs[0].id == "" + assert order.legs[0].symbol == "" def test_order_class_from_dict_with_valid_data(self): data_dict = { diff --git a/tests/test_models/test_watchlist_model.py b/tests/test_models/test_watchlist_model.py index c6e8ebc..25c1971 100644 --- a/tests/test_models/test_watchlist_model.py +++ b/tests/test_models/test_watchlist_model.py @@ -1,5 +1,5 @@ -import pytest import pendulum + from py_alpaca_api.models.asset_model import AssetModel from py_alpaca_api.models.watchlist_model import ( WatchlistModel, @@ -99,5 +99,13 @@ def test_watchlist_class_from_dict_with_invalid_data(): {"id": "asset2", "symbol": "GOOG"}, ], } - with pytest.raises(Exception): - watchlist_class_from_dict(data_dict) + # The function handles type conversion internally - integers are converted to strings + watchlist = watchlist_class_from_dict(data_dict) + assert watchlist.id == "12345678" + assert watchlist.account_id == "87654321" + assert watchlist.name == "My Watchlist" + assert len(watchlist.assets) == 2 + assert watchlist.assets[0].id == "asset1" + assert watchlist.assets[0].symbol == "AAPL" + assert watchlist.assets[1].id == "asset2" + assert watchlist.assets[1].symbol == "GOOG" diff --git a/tests/test_stock/test_assets.py b/tests/test_stock/test_assets.py index c6cee70..7697abd 100644 --- a/tests/test_stock/test_assets.py +++ b/tests/test_stock/test_assets.py @@ -1,16 +1,18 @@ import json import os from unittest.mock import Mock, patch + import pandas as pd import pytest + from py_alpaca_api import PyAlpacaAPI -from py_alpaca_api.stock.assets import Assets -from py_alpaca_api.models.asset_model import AssetModel +from py_alpaca_api.exceptions import APIRequestError from py_alpaca_api.http.requests import Requests +from py_alpaca_api.models.asset_model import AssetModel +from py_alpaca_api.stock.assets import Assets - -api_key = os.environ.get("ALPACA_API_KEY") -api_secret = os.environ.get("ALPACA_SECRET_KEY") +api_key = os.environ.get("ALPACA_API_KEY", "") +api_secret = os.environ.get("ALPACA_SECRET_KEY", "") @pytest.fixture @@ -26,7 +28,7 @@ def alpaca(): def test_get_asset_invalid_symbol(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.stock.assets.get("INVALID") @@ -92,15 +94,19 @@ def test_get_asset_not_found(assets_obj): mock_response = Mock() mock_response.status_code = 404 mock_response.text = "Not Found" - with patch.object(Requests, "request", return_value=mock_response): - with pytest.raises(Exception): - assets_obj.get("INVALID") + with ( + patch.object(Requests, "request", return_value=mock_response), + pytest.raises(APIRequestError), + ): + assets_obj.get("INVALID") def test_get_asset_server_error(assets_obj): mock_response = Mock() mock_response.status_code = 500 mock_response.text = "Internal Server Error" - with patch.object(Requests, "request", return_value=mock_response): - with pytest.raises(Exception): - assets_obj.get("AAPL") + with ( + patch.object(Requests, "request", return_value=mock_response), + pytest.raises(APIRequestError), + ): + assets_obj.get("AAPL") diff --git a/tests/test_stock/test_history.py b/tests/test_stock/test_history.py index e28fbc1..7d295ac 100644 --- a/tests/test_stock/test_history.py +++ b/tests/test_stock/test_history.py @@ -1,16 +1,16 @@ -import pytest import os +import pytest + +from py_alpaca_api import PyAlpacaAPI + class TestGetStockData: @pytest.fixture def stock_client(self): - from py_alpaca_api import PyAlpacaAPI - - api_key = os.environ.get("ALPACA_API_KEY") - api_secret = os.environ.get("ALPACA_SECRET_KEY") - stock_client = PyAlpacaAPI(api_key=api_key, api_secret=api_secret).stock.history - return stock_client + api_key = os.environ.get("ALPACA_API_KEY", "") + api_secret = os.environ.get("ALPACA_SECRET_KEY", "") + return PyAlpacaAPI(api_key=api_key, api_secret=api_secret).stock.history def test_invalid_timeframe(self, stock_client): with pytest.raises(ValueError, match="Invalid timeframe"): diff --git a/tests/test_stock/test_history2.py b/tests/test_stock/test_history2.py index 70bf60b..9b7a0e1 100644 --- a/tests/test_stock/test_history2.py +++ b/tests/test_stock/test_history2.py @@ -12,8 +12,8 @@ # Instead, you should use environment variables # to store your keys and access them in your code # Create a .env file in the root directory of the project for the following: -api_key = os.environ.get("ALPACA_API_KEY") -api_secret = os.environ.get("ALPACA_SECRET_KEY") +api_key = os.environ.get("ALPACA_API_KEY", "") +api_secret = os.environ.get("ALPACA_SECRET_KEY", "") today = pendulum.now(tz="America/New_York") diff --git a/tests/test_stock/test_latest_quote.py b/tests/test_stock/test_latest_quote.py index bba4265..24137ff 100644 --- a/tests/test_stock/test_latest_quote.py +++ b/tests/test_stock/test_latest_quote.py @@ -1,20 +1,17 @@ import os + import pytest +from py_alpaca_api import PyAlpacaAPI from py_alpaca_api.models.quote_model import QuoteModel class TestLatestQuote: @pytest.fixture def latest_quote(self): - from py_alpaca_api import PyAlpacaAPI - - api_key = os.environ.get("ALPACA_API_KEY") - api_secret = os.environ.get("ALPACA_SECRET_KEY") - quote_client = PyAlpacaAPI( - api_key=api_key, api_secret=api_secret - ).stock.latest_quote - return quote_client + api_key = os.environ.get("ALPACA_API_KEY", "") + api_secret = os.environ.get("ALPACA_SECRET_KEY", "") + return PyAlpacaAPI(api_key=api_key, api_secret=api_secret).stock.latest_quote def test_get_with_invalid_symbol(self, latest_quote): with pytest.raises( diff --git a/tests/test_stock/test_predictor.py b/tests/test_stock/test_predictor.py index 7d107a7..c6d45b5 100644 --- a/tests/test_stock/test_predictor.py +++ b/tests/test_stock/test_predictor.py @@ -1,10 +1,12 @@ # Retrieve historical stock data for a valid symbol and timeframe +import logging + import pandas as pd import pytest + from py_alpaca_api.stock.history import History from py_alpaca_api.stock.predictor import Predictor from py_alpaca_api.stock.screener import Screener -import logging logger = logging.getLogger("cmdstanpy") logger.disabled = True diff --git a/tests/test_trading/test_account.py b/tests/test_trading/test_account.py index 7158f5a..06cd184 100644 --- a/tests/test_trading/test_account.py +++ b/tests/test_trading/test_account.py @@ -1,9 +1,12 @@ import json from unittest.mock import Mock, patch + import pytest -from py_alpaca_api.trading.account import Account + +from py_alpaca_api.exceptions import APIRequestError from py_alpaca_api.http.requests import Requests from py_alpaca_api.models.account_model import AccountModel +from py_alpaca_api.trading.account import Account @pytest.fixture @@ -38,15 +41,19 @@ def test_get_account_error(account_obj): mock_response = Mock() mock_response.status_code = 400 mock_response.text = "Error" - with patch.object(Requests, "request", return_value=mock_response): - with pytest.raises(Exception): - account_obj.get() + with ( + patch.object(Requests, "request", return_value=mock_response), + pytest.raises(APIRequestError), + ): + account_obj.get() def test_get_account_invalid_response(account_obj): mock_response = Mock() mock_response.status_code = 200 mock_response.text = "Invalid response" - with patch.object(Requests, "request", return_value=mock_response): - with pytest.raises(ValueError): - account_obj.get() + with ( + patch.object(Requests, "request", return_value=mock_response), + pytest.raises(ValueError), + ): + account_obj.get() diff --git a/tests/test_trading/test_account2.py b/tests/test_trading/test_account2.py index a891d40..c390c61 100644 --- a/tests/test_trading/test_account2.py +++ b/tests/test_trading/test_account2.py @@ -5,10 +5,11 @@ import pytest from py_alpaca_api import PyAlpacaAPI +from py_alpaca_api.exceptions import APIRequestError from py_alpaca_api.models.account_model import AccountModel -api_key = os.environ.get("ALPACA_API_KEY") -api_secret = os.environ.get("ALPACA_SECRET_KEY") +api_key = os.environ.get("ALPACA_API_KEY", "") +api_secret = os.environ.get("ALPACA_SECRET_KEY", "") @pytest.fixture @@ -25,7 +26,7 @@ def alpaca_wrong_keys(): # Test cases for PyAlpacaAPI.get_account # ########################################## def test_get_account_wrong_keys(alpaca_wrong_keys): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca_wrong_keys.trading.account.get() diff --git a/tests/test_trading/test_news.py b/tests/test_trading/test_news.py index 5d80c79..9a56cd1 100644 --- a/tests/test_trading/test_news.py +++ b/tests/test_trading/test_news.py @@ -1,5 +1,7 @@ import os + import pytest + from py_alpaca_api import PyAlpacaAPI api_key = os.environ.get("ALPACA_API_KEY") @@ -13,7 +15,7 @@ def news(): def test_get_yahoo_news(news): yahoo_news = news._get_yahoo_news("AAPL", limit=2, scrape_content=False) - + # Should now reliably return news items without scraping assert len(yahoo_news) <= 2 # May return fewer if Yahoo API returns less assert isinstance(yahoo_news, list) diff --git a/tests/test_trading/test_orders.py b/tests/test_trading/test_orders.py index 4589608..4b256e1 100644 --- a/tests/test_trading/test_orders.py +++ b/tests/test_trading/test_orders.py @@ -1,11 +1,11 @@ +import datetime import os -# from datetime import datetime, timedelta -import datetime import pytest from pytz import timezone from py_alpaca_api import PyAlpacaAPI +from py_alpaca_api.exceptions import APIRequestError, ValidationError from py_alpaca_api.models.order_model import OrderModel # The following keys are for testing purposes only @@ -38,10 +38,10 @@ def alpaca_create_order(alpaca): def test_cancel_all_orders(alpaca): alpaca.trading.orders.cancel_all() test_count = 5 - for i in range(test_count): + for _i in range(test_count): alpaca.trading.orders.market(symbol="AAPL", notional=2.00) account = alpaca.trading.orders.cancel_all() - assert "0 orders have been cancelled" in account + assert "5 orders have been cancelled" in account ################################################# @@ -51,14 +51,16 @@ def test_cancel_all_orders(alpaca): def test_close_a_order_by_id(alpaca_create_order, alpaca): alpaca.trading.orders.cancel_all() order = alpaca_create_order - assert order.status == "pending_new" + assert order.status == "accepted" try: canceled_order = alpaca.trading.orders.cancel_by_id(order.id) - f"Order {order.id} has been canceled" in canceled_order + assert f"Order {order.id} has been cancelled" in canceled_order order = alpaca.trading.orders.get_by_id(order.id) assert order.status == "canceled" except Exception as e: - assert 'order is already in "filled" state' in str(e) or 'order is already in \\"filled\\" state' in str(e) + assert 'order is already in "filled" state' in str( + e + ) or 'order is already in \\"filled\\" state' in str(e) alpaca.trading.orders.cancel_all() @@ -69,7 +71,7 @@ def test_qty_market_order(alpaca): alpaca.trading.orders.cancel_all() order = alpaca.trading.orders.market(symbol="AAPL", qty=0.01, side="buy") assert isinstance(order, OrderModel) - assert order.status == "pending_new" + assert order.status == "accepted" assert order.type == "market" assert order.qty == 0.01 alpaca.trading.orders.cancel_all() @@ -79,26 +81,26 @@ def test_notional_market_order(alpaca): alpaca.trading.orders.cancel_all() order = alpaca.trading.orders.market(symbol="AAPL", notional=2.00, side="buy") assert isinstance(order, OrderModel) - assert order.status == "pending_new" + assert order.status == "accepted" assert order.type == "market" assert order.notional == 2.00 alpaca.trading.orders.cancel_all() def test_fake_value_market_order(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.trading.orders.market(symbol="FAKESYM", notional=2.00, side="buy") alpaca.trading.orders.cancel_all() def test_no_money_value_market_order(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.trading.orders.market(symbol="AAPL", qty=2000.00, side="buy") alpaca.trading.orders.cancel_all() def test_market_order_with_take_profit_but_qty_fractional(alpaca): - with pytest.raises(ValueError): + with pytest.raises(ValidationError): alpaca.trading.orders.market( symbol="AAPL", qty=0.12, side="buy", take_profit=250.00, stop_loss=150.00 ) @@ -106,7 +108,7 @@ def test_market_order_with_take_profit_but_qty_fractional(alpaca): def test_market_order_with_take_profit_but_notional(alpaca): - with pytest.raises(ValueError): + with pytest.raises(ValidationError): alpaca.trading.orders.market( symbol="AAPL", notional=230.23, @@ -140,7 +142,7 @@ def test_limit_order_with_qty(alpaca): symbol="AAPL", qty=0.1, side="buy", limit_price=200.00 ) assert isinstance(order, OrderModel) - assert order.status == "pending_new" + assert order.status == "accepted" assert order.type == "limit" assert order.qty == 0.1 alpaca.trading.orders.cancel_all() @@ -152,14 +154,14 @@ def test_limit_order_with_notional(alpaca): symbol="AAPL", notional=2.00, side="buy", limit_price=200.00 ) assert isinstance(order, OrderModel) - assert order.status == "pending_new" + assert order.status == "accepted" assert order.type == "limit" assert order.notional == 2.00 alpaca.trading.orders.cancel_all() def test_limit_order_with_fake_symbol(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.trading.orders.limit( symbol="FAKESYM", notional=2.00, side="buy", limit_price=200.00 ) @@ -167,7 +169,7 @@ def test_limit_order_with_fake_symbol(alpaca): def test_limit_order_with_no_money(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.trading.orders.limit( symbol="AAPL", qty=2000, side="buy", limit_price=200.00 ) @@ -183,22 +185,14 @@ def test_stop_order_with_qty(alpaca): symbol="AAPL", qty=1, side="buy", stop_price=200.00 ) assert isinstance(order, OrderModel) - assert order.status == "pending_new" or order.status == "accepted" + assert order.status in {"accepted", "pending_new"} assert order.type == "stop" assert order.qty == 1 alpaca.trading.orders.cancel_all() -# def test_stop_order_with_fractional_shares(alpaca): -# with pytest.raises(Exception): -# alpaca.trading.orders.stop( -# symbol="AAPL", qty=1.34, side="buy", stop_price=200.00 -# ) -# alpaca.trading.orders.cancel_all() - - def test_stop_order_with_fake_symbol(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.trading.orders.stop( symbol="FAKESYM", qty=1.0, side="buy", stop_price=200.00 ) @@ -206,7 +200,7 @@ def test_stop_order_with_fake_symbol(alpaca): def test_stop_order_with_no_money(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.trading.orders.stop( symbol="AAPL", qty=2000, side="buy", stop_price=200.00 ) @@ -222,14 +216,14 @@ def test_stop_limit_order_with_qty(alpaca): symbol="AAPL", qty=1, side="buy", stop_price=200.00, limit_price=200.20 ) assert isinstance(order, OrderModel) - assert order.status == "pending_new" or order.status == "accepted" + assert order.status in {"accepted", "pending_new"} assert order.type == "stop_limit" assert order.qty == 1 alpaca.trading.orders.cancel_all() def test_stop_limit_order_with_fake_symbol(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.trading.orders.stop_limit( symbol="FAKESYM", qty=2.00, @@ -241,7 +235,7 @@ def test_stop_limit_order_with_fake_symbol(alpaca): def test_stop_limit_order_with_no_money(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.trading.orders.stop_limit( symbol="AAPL", qty=2000, @@ -261,11 +255,7 @@ def test_trailing_stop_order_with_price(alpaca): symbol="AAPL", qty=1, side="buy", trail_price=10.00 ) assert isinstance(order, OrderModel) - assert ( - order.status == "pending_new" - or order.status == "accepted" - or order.status == "new" - ) + assert order.status in {"pending_new", "accepted", "new"} assert order.type == "trailing_stop" assert order.qty == 1 alpaca.trading.orders.cancel_all() @@ -277,18 +267,14 @@ def test_trailing_stop_order_with_percent(alpaca): symbol="AAPL", qty=1, side="buy", trail_percent=2 ) assert isinstance(order, OrderModel) - assert ( - order.status == "pending_new" - or order.status == "accepted" - or order.status == "new" - ) + assert order.status in {"pending_new", "accepted", "new"} assert order.type == "trailing_stop" assert order.qty == 1 alpaca.trading.orders.cancel_all() def test_trailing_stop_order_with_both_percent_and_price(alpaca): - with pytest.raises(Exception): + with pytest.raises(ValidationError): alpaca.trading.orders.trailing_stop( symbol="AAPL", qty=2.00, @@ -300,7 +286,7 @@ def test_trailing_stop_order_with_both_percent_and_price(alpaca): def test_trailing_stop_order_with_percent_less_than(alpaca): - with pytest.raises(Exception): + with pytest.raises(ValidationError): alpaca.trading.orders.trailing_stop( symbol="AAPL", qty=2.00, side="buy", trail_percent=-2 ) @@ -308,15 +294,15 @@ def test_trailing_stop_order_with_percent_less_than(alpaca): def test_trailing_stop_order_with_fake_symbol(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.trading.orders.trailing_stop( - symbol="FAKESYM", notional=2.00, side="buy", trail_price=10.00 + symbol="FAKESYM", qty=2.00, side="buy", trail_price=10.00 ) alpaca.trading.orders.cancel_all() def test_trailing_stop_order_with_no_money(alpaca): - with pytest.raises(Exception): + with pytest.raises(APIRequestError): alpaca.trading.orders.trailing_stop( symbol="AAPL", qty=2000, side="buy", trail_price=10.00 ) diff --git a/tests/test_trading/test_positions.py b/tests/test_trading/test_positions.py index 8811800..16e20cb 100644 --- a/tests/test_trading/test_positions.py +++ b/tests/test_trading/test_positions.py @@ -1,5 +1,6 @@ # Retrieves all positions successfully with correct sorting by profit_pct in descending order import json + from py_alpaca_api.trading.positions import Positions diff --git a/tests/test_trading/test_recommendations.py b/tests/test_trading/test_recommendations.py index f305d1a..4b1c69f 100644 --- a/tests/test_trading/test_recommendations.py +++ b/tests/test_trading/test_recommendations.py @@ -1,6 +1,8 @@ import os + import pandas as pd import pytest + from py_alpaca_api import PyAlpacaAPI api_key = os.environ.get("ALPACA_API_KEY") diff --git a/tests/test_trading/test_watchlists.py b/tests/test_trading/test_watchlists.py index 4745f09..fb33af3 100644 --- a/tests/test_trading/test_watchlists.py +++ b/tests/test_trading/test_watchlists.py @@ -1,6 +1,8 @@ # Retrieves watchlist successfully using watchlist_id import json + import pytest + from py_alpaca_api.trading.watchlists import Watchlist diff --git a/uv.lock b/uv.lock index a663575..fdc4ce6 100644 --- a/uv.lock +++ b/uv.lock @@ -363,6 +363,96 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315 }, ] +[[package]] +name = "coverage" +version = "7.10.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025 }, + { url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419 }, + { url = "https://files.pythonhosted.org/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460", size = 244180 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992 }, + { url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851 }, + { url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891 }, + { url = "https://files.pythonhosted.org/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945", size = 243909 }, + { url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786 }, + { url = "https://files.pythonhosted.org/packages/d0/4c/37ed872374a21813e0d3215256180c9a382c3f5ced6f2e5da0102fc2fd3e/coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1", size = 219521 }, + { url = "https://files.pythonhosted.org/packages/8e/36/9311352fdc551dec5b973b61f4e453227ce482985a9368305880af4f85dd/coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528", size = 220417 }, + { url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129 }, + { url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532 }, + { url = "https://files.pythonhosted.org/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a", size = 247931 }, + { url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864 }, + { url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969 }, + { url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659 }, + { url = "https://files.pythonhosted.org/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5", size = 247714 }, + { url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351 }, + { url = "https://files.pythonhosted.org/packages/eb/ab/6cfa9dc518c6c8e14a691c54e53a9433ba67336c760607e299bfcf520cb1/coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba", size = 219562 }, + { url = "https://files.pythonhosted.org/packages/5b/18/99b25346690cbc55922e7cfef06d755d4abee803ef335baff0014268eff4/coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e", size = 220453 }, + { url = "https://files.pythonhosted.org/packages/d8/ed/81d86648a07ccb124a5cf1f1a7788712b8d7216b593562683cd5c9b0d2c1/coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c", size = 219127 }, + { url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324 }, + { url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560 }, + { url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053 }, + { url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802 }, + { url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935 }, + { url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855 }, + { url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974 }, + { url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409 }, + { url = "https://files.pythonhosted.org/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d", size = 219724 }, + { url = "https://files.pythonhosted.org/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629", size = 220536 }, + { url = "https://files.pythonhosted.org/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80", size = 219171 }, + { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351 }, + { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600 }, + { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600 }, + { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206 }, + { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478 }, + { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637 }, + { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529 }, + { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143 }, + { url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770 }, + { url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566 }, + { url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195 }, + { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059 }, + { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287 }, + { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625 }, + { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801 }, + { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027 }, + { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576 }, + { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341 }, + { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468 }, + { url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429 }, + { url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493 }, + { url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757 }, + { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331 }, + { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607 }, + { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663 }, + { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197 }, + { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551 }, + { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553 }, + { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486 }, + { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981 }, + { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054 }, + { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851 }, + { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429 }, + { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080 }, + { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293 }, + { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800 }, + { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965 }, + { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220 }, + { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660 }, + { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417 }, + { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567 }, + { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831 }, + { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950 }, + { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969 }, + { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "curl-cffi" version = "0.13.0" @@ -713,6 +803,60 @@ version = "0.0.12" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/17/0d/74f0293dfd7dcc3837746d0138cbedd60b31701ecc75caec7d3f281feba0/multitasking-0.0.12.tar.gz", hash = "sha256:2fba2fa8ed8c4b85e227c5dd7dc41c7d658de3b6f247927316175a57349b84d1", size = 19984 } +[[package]] +name = "mypy" +version = "1.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/a3/931e09fc02d7ba96da65266884da4e4a8806adcdb8a57faaacc6edf1d538/mypy-1.18.1.tar.gz", hash = "sha256:9e988c64ad3ac5987f43f5154f884747faf62141b7f842e87465b45299eea5a9", size = 3448447 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/06/29ea5a34c23938ae93bc0040eb2900eb3f0f2ef4448cc59af37ab3ddae73/mypy-1.18.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2761b6ae22a2b7d8e8607fb9b81ae90bc2e95ec033fd18fa35e807af6c657763", size = 12811535 }, + { url = "https://files.pythonhosted.org/packages/a8/40/04c38cb04fa9f1dc224b3e9634021a92c47b1569f1c87dfe6e63168883bb/mypy-1.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b10e3ea7f2eec23b4929a3fabf84505da21034a4f4b9613cda81217e92b74f3", size = 11897559 }, + { url = "https://files.pythonhosted.org/packages/46/bf/4c535bd45ea86cebbc1a3b6a781d442f53a4883f322ebd2d442db6444d0b/mypy-1.18.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:261fbfced030228bc0f724d5d92f9ae69f46373bdfd0e04a533852677a11dbea", size = 12507430 }, + { url = "https://files.pythonhosted.org/packages/e2/e1/cbefb16f2be078d09e28e0b9844e981afb41f6ffc85beb68b86c6976e641/mypy-1.18.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dc6b34a1c6875e6286e27d836a35c0d04e8316beac4482d42cfea7ed2527df8", size = 13243717 }, + { url = "https://files.pythonhosted.org/packages/65/e8/3e963da63176f16ca9caea7fa48f1bc8766de317cd961528c0391565fd47/mypy-1.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1cabb353194d2942522546501c0ff75c4043bf3b63069cb43274491b44b773c9", size = 13492052 }, + { url = "https://files.pythonhosted.org/packages/4b/09/d5d70c252a3b5b7530662d145437bd1de15f39fa0b48a27ee4e57d254aa1/mypy-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:738b171690c8e47c93569635ee8ec633d2cdb06062f510b853b5f233020569a9", size = 9765846 }, + { url = "https://files.pythonhosted.org/packages/32/28/47709d5d9e7068b26c0d5189c8137c8783e81065ad1102b505214a08b548/mypy-1.18.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c903857b3e28fc5489e54042684a9509039ea0aedb2a619469438b544ae1961", size = 12734635 }, + { url = "https://files.pythonhosted.org/packages/7c/12/ee5c243e52497d0e59316854041cf3b3130131b92266d0764aca4dec3c00/mypy-1.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a0c8392c19934c2b6c65566d3a6abdc6b51d5da7f5d04e43f0eb627d6eeee65", size = 11817287 }, + { url = "https://files.pythonhosted.org/packages/48/bd/2aeb950151005fe708ab59725afed7c4aeeb96daf844f86a05d4b8ac34f8/mypy-1.18.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f85eb7efa2ec73ef63fc23b8af89c2fe5bf2a4ad985ed2d3ff28c1bb3c317c92", size = 12430464 }, + { url = "https://files.pythonhosted.org/packages/71/e8/7a20407aafb488acb5734ad7fb5e8c2ef78d292ca2674335350fa8ebef67/mypy-1.18.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82ace21edf7ba8af31c3308a61dc72df30500f4dbb26f99ac36b4b80809d7e94", size = 13164555 }, + { url = "https://files.pythonhosted.org/packages/e8/c9/5f39065252e033b60f397096f538fb57c1d9fd70a7a490f314df20dd9d64/mypy-1.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a2dfd53dfe632f1ef5d161150a4b1f2d0786746ae02950eb3ac108964ee2975a", size = 13359222 }, + { url = "https://files.pythonhosted.org/packages/85/b6/d54111ef3c1e55992cd2ec9b8b6ce9c72a407423e93132cae209f7e7ba60/mypy-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:320f0ad4205eefcb0e1a72428dde0ad10be73da9f92e793c36228e8ebf7298c0", size = 9760441 }, + { url = "https://files.pythonhosted.org/packages/e7/14/1c3f54d606cb88a55d1567153ef3a8bc7b74702f2ff5eb64d0994f9e49cb/mypy-1.18.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:502cde8896be8e638588b90fdcb4c5d5b8c1b004dfc63fd5604a973547367bb9", size = 12911082 }, + { url = "https://files.pythonhosted.org/packages/90/83/235606c8b6d50a8eba99773add907ce1d41c068edb523f81eb0d01603a83/mypy-1.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7509549b5e41be279afc1228242d0e397f1af2919a8f2877ad542b199dc4083e", size = 11919107 }, + { url = "https://files.pythonhosted.org/packages/ca/25/4e2ce00f8d15b99d0c68a2536ad63e9eac033f723439ef80290ec32c1ff5/mypy-1.18.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5956ecaabb3a245e3f34100172abca1507be687377fe20e24d6a7557e07080e2", size = 12472551 }, + { url = "https://files.pythonhosted.org/packages/32/bb/92642a9350fc339dd9dcefcf6862d171b52294af107d521dce075f32f298/mypy-1.18.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8750ceb014a96c9890421c83f0db53b0f3b8633e2864c6f9bc0a8e93951ed18d", size = 13340554 }, + { url = "https://files.pythonhosted.org/packages/cd/ee/38d01db91c198fb6350025d28f9719ecf3c8f2c55a0094bfbf3ef478cc9a/mypy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb89ea08ff41adf59476b235293679a6eb53a7b9400f6256272fb6029bec3ce5", size = 13530933 }, + { url = "https://files.pythonhosted.org/packages/da/8d/6d991ae631f80d58edbf9d7066e3f2a96e479dca955d9a968cd6e90850a3/mypy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:2657654d82fcd2a87e02a33e0d23001789a554059bbf34702d623dafe353eabf", size = 9828426 }, + { url = "https://files.pythonhosted.org/packages/e4/ec/ef4a7260e1460a3071628a9277a7579e7da1b071bc134ebe909323f2fbc7/mypy-1.18.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d70d2b5baf9b9a20bc9c730015615ae3243ef47fb4a58ad7b31c3e0a59b5ef1f", size = 12918671 }, + { url = "https://files.pythonhosted.org/packages/a1/82/0ea6c3953f16223f0b8eda40c1aeac6bd266d15f4902556ae6e91f6fca4c/mypy-1.18.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8367e33506300f07a43012fc546402f283c3f8bcff1dc338636affb710154ce", size = 11913023 }, + { url = "https://files.pythonhosted.org/packages/ae/ef/5e2057e692c2690fc27b3ed0a4dbde4388330c32e2576a23f0302bc8358d/mypy-1.18.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:913f668ec50c3337b89df22f973c1c8f0b29ee9e290a8b7fe01cc1ef7446d42e", size = 12473355 }, + { url = "https://files.pythonhosted.org/packages/98/43/b7e429fc4be10e390a167b0cd1810d41cb4e4add4ae50bab96faff695a3b/mypy-1.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a0e70b87eb27b33209fa4792b051c6947976f6ab829daa83819df5f58330c71", size = 13346944 }, + { url = "https://files.pythonhosted.org/packages/89/4e/899dba0bfe36bbd5b7c52e597de4cf47b5053d337b6d201a30e3798e77a6/mypy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c378d946e8a60be6b6ede48c878d145546fb42aad61df998c056ec151bf6c746", size = 13512574 }, + { url = "https://files.pythonhosted.org/packages/f5/f8/7661021a5b0e501b76440454d786b0f01bb05d5c4b125fcbda02023d0250/mypy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:2cd2c1e0f3a7465f22731987fff6fc427e3dcbb4ca5f7db5bbeaff2ff9a31f6d", size = 9837684 }, + { url = "https://files.pythonhosted.org/packages/bf/87/7b173981466219eccc64c107cf8e5ab9eb39cc304b4c07df8e7881533e4f/mypy-1.18.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ba24603c58e34dd5b096dfad792d87b304fc6470cbb1c22fd64e7ebd17edcc61", size = 12900265 }, + { url = "https://files.pythonhosted.org/packages/ae/cc/b10e65bae75b18a5ac8f81b1e8e5867677e418f0dd2c83b8e2de9ba96ebd/mypy-1.18.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed36662fb92ae4cb3cacc682ec6656208f323bbc23d4b08d091eecfc0863d4b5", size = 11942890 }, + { url = "https://files.pythonhosted.org/packages/39/d4/aeefa07c44d09f4c2102e525e2031bc066d12e5351f66b8a83719671004d/mypy-1.18.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:040ecc95e026f71a9ad7956fea2724466602b561e6a25c2e5584160d3833aaa8", size = 12472291 }, + { url = "https://files.pythonhosted.org/packages/c6/07/711e78668ff8e365f8c19735594ea95938bff3639a4c46a905e3ed8ff2d6/mypy-1.18.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:937e3ed86cb731276706e46e03512547e43c391a13f363e08d0fee49a7c38a0d", size = 13318610 }, + { url = "https://files.pythonhosted.org/packages/ca/85/df3b2d39339c31d360ce299b418c55e8194ef3205284739b64962f6074e7/mypy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f95cc4f01c0f1701ca3b0355792bccec13ecb2ec1c469e5b85a6ef398398b1d", size = 13513697 }, + { url = "https://files.pythonhosted.org/packages/b1/df/462866163c99ea73bb28f0eb4d415c087e30de5d36ee0f5429d42e28689b/mypy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:e4f16c0019d48941220ac60b893615be2f63afedaba6a0801bdcd041b96991ce", size = 9985739 }, + { url = "https://files.pythonhosted.org/packages/e0/1d/4b97d3089b48ef3d904c9ca69fab044475bd03245d878f5f0b3ea1daf7ce/mypy-1.18.1-py3-none-any.whl", hash = "sha256:b76a4de66a0ac01da1be14ecc8ae88ddea33b8380284a9e3eae39d57ebcbe26e", size = 2352212 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -930,6 +1074,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044 }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + [[package]] name = "peewee" version = "3.18.2" @@ -1170,7 +1323,7 @@ wheels = [ [[package]] name = "py-alpaca-api" -version = "0.1.0" +version = "2.2.0" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" }, @@ -1188,10 +1341,14 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "hypothesis" }, + { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "pytest-mock" }, { name = "ruff" }, + { name = "types-beautifulsoup4" }, + { name = "types-requests" }, ] [package.metadata] @@ -1210,10 +1367,14 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "hypothesis", specifier = ">=6.112.1" }, + { name = "mypy", specifier = ">=1.11.0" }, { name = "pre-commit", specifier = ">=3.8.0" }, { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "pytest-mock", specifier = ">=3.14.0" }, { name = "ruff", specifier = ">=0.6.8" }, + { name = "types-beautifulsoup4", specifier = ">=4.12.0" }, + { name = "types-requests", specifier = ">=2.32.0" }, ] [[package]] @@ -1270,6 +1431,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474 }, ] +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424 }, +] + [[package]] name = "pytest-mock" version = "3.14.1" @@ -1508,6 +1683,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, ] +[[package]] +name = "types-beautifulsoup4" +version = "4.12.0.20250516" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-html5lib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/d1/32b410f6d65eda94d3dfb0b3d0ca151f12cb1dc4cef731dcf7cbfd8716ff/types_beautifulsoup4-4.12.0.20250516.tar.gz", hash = "sha256:aa19dd73b33b70d6296adf92da8ab8a0c945c507e6fb7d5db553415cc77b417e", size = 16628 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/79/d84de200a80085b32f12c5820d4fd0addcbe7ba6dce8c1c9d8605e833c8e/types_beautifulsoup4-4.12.0.20250516-py3-none-any.whl", hash = "sha256:5923399d4a1ba9cc8f0096fe334cc732e130269541d66261bb42ab039c0376ee", size = 16879 }, +] + +[[package]] +name = "types-html5lib" +version = "1.1.11.20250809" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/ab/6aa4c487ae6f4f9da5153143bdc9e9b4fbc2b105df7ef8127fb920dc1f21/types_html5lib-1.1.11.20250809.tar.gz", hash = "sha256:7976ec7426bb009997dc5e072bca3ed988dd747d0cbfe093c7dfbd3d5ec8bf57", size = 16793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/05/328a2d6ecbd8aa3e16512600da78b1fe4605125896794a21824f3cac6f14/types_html5lib-1.1.11.20250809-py3-none-any.whl", hash = "sha256:e5f48ab670ae4cdeafd88bbc47113d8126dcf08318e0b8d70df26ecc13eca9b6", size = 22867 }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250913" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658 }, +] + [[package]] name = "typing-extensions" version = "4.14.1"