From f4ff1beda4a2329d3694cf96510da3be31c0aa3f Mon Sep 17 00:00:00 2001 From: Sean N Date: Mon, 11 Aug 2025 13:25:26 +0200 Subject: [PATCH 1/3] Fixed: tests and adding skeleton ci/cd actions --- .github/workflows/ci.yml | 99 ++++++++++++++++++++++ .github/workflows/publish.yml | 89 ++++++++++++++++++++ .github/workflows/release.yml | 96 ++++++++++++++++++++++ .gitignore | 3 +- ohheycrypto/config/settings.py | 23 ++++-- pyproject.toml | 3 - tests/conftest.py | 14 ++-- tests/test_cli.py | 146 +++++++++++++++++++++++++++++++++ tests/test_config.py | 47 ++++++----- tests/test_discord.py | 18 ++-- tests/test_wallet.py | 42 ++++++---- 11 files changed, 515 insertions(+), 65 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 tests/test_cli.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..269c669 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,99 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Lint with flake8 + run: | + # Stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # Exit-zero treats all errors as warnings + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics + + - name: Format check with black + run: | + black --check --diff . + + - name: Sort imports check with isort + run: | + isort --check-only --diff . + + - name: Type check with mypy + run: | + mypy ohheycrypto --ignore-missing-imports + continue-on-error: true # Don't fail CI on mypy errors yet + + - name: Security check with bandit + run: | + bandit -r ohheycrypto -f json -o bandit-report.json || true + bandit -r ohheycrypto + continue-on-error: true + + - name: Test CLI functionality + run: | + # Test that the CLI can be imported and basic commands work + ohheycrypto --version + ohheycrypto init test_config.json + ohheycrypto validate test_config.json || true # Expected to fail without API keys + + - name: Run tests + run: | + pytest --cov=ohheycrypto --cov-report=xml --cov-report=term-missing -v + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + build-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Test installation + run: | + pip install dist/*.whl + ohheycrypto --version \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..acf17b3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,89 @@ +name: Publish to PyPI + +on: + release: + types: [published] + workflow_dispatch: # Allow manual triggering + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests + run: | + pytest --cov=ohheycrypto --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + build: + needs: test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + environment: + name: pypi + url: https://pypi.org/p/ohheycrypto + permissions: + id-token: write # For trusted publishing + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v3 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..221a581 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,96 @@ +name: Create Release + +on: + push: + tags: + - 'v*.*.*' # Triggers on version tags like v0.2.0, v1.0.0 + +jobs: + create-release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch full history for changelog + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + pip install -e ".[dev]" + + - name: Run tests + run: | + pytest --cov=ohheycrypto + + - name: Build package + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Extract version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Generate changelog + id: changelog + run: | + # Simple changelog generation - you can improve this + echo "CHANGELOG<> $GITHUB_OUTPUT + echo "## What's Changed" >> $GITHUB_OUTPUT + git log --pretty=format:"- %s" $(git describe --tags --abbrev=0 HEAD^)..HEAD >> $GITHUB_OUTPUT + echo "" >> $GITHUB_OUTPUT + echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/$(git describe --tags --abbrev=0 HEAD^)...v${{ steps.get_version.outputs.VERSION }}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release v${{ steps.get_version.outputs.VERSION }} + body: | + # OhHeyCrypto v${{ steps.get_version.outputs.VERSION }} + + ${{ steps.changelog.outputs.CHANGELOG }} + + ## Installation + + ```bash + pip install ohheycrypto==${{ steps.get_version.outputs.VERSION }} + ``` + + ## Quick Start + + ```bash + ohheycrypto init config.json + # Edit config.json with your API keys + ohheycrypto validate config.json + ohheycrypto run config.json + ``` + + ## Docker + + ```bash + docker run -v $(pwd)/config.json:/app/config.json ohheycrypto:v${{ steps.get_version.outputs.VERSION }} + ``` + draft: false + prerelease: ${{ contains(steps.get_version.outputs.VERSION, 'alpha') || contains(steps.get_version.outputs.VERSION, 'beta') || contains(steps.get_version.outputs.VERSION, 'rc') }} + + - name: Upload Release Assets + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: dist/ohheycrypto-${{ steps.get_version.outputs.VERSION }}-py3-none-any.whl + asset_name: ohheycrypto-${{ steps.get_version.outputs.VERSION }}-py3-none-any.whl + asset_content_type: application/zip \ No newline at end of file diff --git a/.gitignore b/.gitignore index 43b68bd..44a43da 100644 --- a/.gitignore +++ b/.gitignore @@ -86,4 +86,5 @@ logs/ plan.md .claude .cursor -.pycharm \ No newline at end of file +.pycharm +PUBLISHING.md diff --git a/ohheycrypto/config/settings.py b/ohheycrypto/config/settings.py index 92bc966..95eab37 100644 --- a/ohheycrypto/config/settings.py +++ b/ohheycrypto/config/settings.py @@ -3,7 +3,7 @@ import logging from typing import Dict, Any, Optional from pathlib import Path -from pydantic import BaseModel, Field, validator, ValidationError +from pydantic import BaseModel, Field, field_validator, ValidationError logger = logging.getLogger(__name__) @@ -45,7 +45,8 @@ def max_position_size(self) -> float: def trailing_stop_percentage(self) -> float: return self.trailing_stop - @validator('position_sizing') + @field_validator('position_sizing') + @classmethod def validate_position_sizing(cls, v): if v.max < v.min: raise ValueError('max position size must be >= min position size') @@ -70,9 +71,10 @@ def ma_short_period(self) -> int: def ma_long_period(self) -> int: return self.ma_long - @validator('ma_long') - def validate_ma_periods(cls, v, values): - if 'ma_short' in values and v <= values['ma_short']: + @field_validator('ma_long') + @classmethod + def validate_ma_periods(cls, v, info): + if hasattr(info, 'data') and 'ma_short' in info.data and v <= info.data['ma_short']: raise ValueError('ma_long must be > ma_short') return v @@ -125,19 +127,22 @@ class APIConfig(BaseModel): binance_api_secret: Optional[str] = Field(default=None, description="Binance API secret") discord_webhook: Optional[str] = Field(default=None, description="Discord webhook URL") - @validator('binance_api_key') + @field_validator('binance_api_key') + @classmethod def validate_api_key(cls, v): if v and len(v) < 10: raise ValueError("binance_api_key appears to be invalid") return v - @validator('binance_api_secret') + @field_validator('binance_api_secret') + @classmethod def validate_api_secret(cls, v): if v and len(v) < 10: raise ValueError("binance_api_secret appears to be invalid") return v - @validator('discord_webhook') + @field_validator('discord_webhook') + @classmethod def validate_discord_webhook(cls, v): if v and not v.startswith("https://discord.com/api/webhooks/"): raise ValueError("Invalid Discord webhook URL format") @@ -219,7 +224,7 @@ def save_to_file(self, config_path: Path) -> None: try: config_path.parent.mkdir(exist_ok=True) with open(config_path, 'w') as f: - json.dump(self.dict(), f, indent=2) + json.dump(self.model_dump(), f, indent=2) logger.info(f"Configuration saved to {config_path}") except Exception as e: logger.error(f"Error saving config: {e}") diff --git a/pyproject.toml b/pyproject.toml index 372f21a..c4bbed1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,9 +58,6 @@ dev = [ "pre-commit>=4.0.0", ] -[tool.setuptools] -packages = {find = {}} - [tool.setuptools.packages.find] where = ["."] include = ["ohheycrypto*"] diff --git a/tests/conftest.py b/tests/conftest.py index d56d313..b81c615 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -64,24 +64,26 @@ def mock_discord_service(): @pytest.fixture def mock_config(): """Mock configuration for testing.""" - from config.settings import Config, TradingConfig, MarketAnalysisConfig, ExecutionConfig, APIConfig + from ohheycrypto.config.settings import Config, TradingConfig, MarketAnalysisConfig, ExecutionConfig, APIConfig return Config( trading=TradingConfig( stop_loss=3.0, sell_threshold=0.4, buy_threshold=0.2, - fiat_currency="USDT", - crypto_currency="BTC" + fiat="USDT", + crypto="BTC" ), market_analysis=MarketAnalysisConfig( rsi_period=14, rsi_oversold=30.0, - rsi_overbought=70.0 + rsi_overbought=70.0, + ma_short=10, + ma_long=20 ), execution=ExecutionConfig( - market_check_interval=60, - circuit_breaker_threshold=5 + check_interval=60, + circuit_breaker={"max_failures": 5, "cooldown": 3600} ), api=APIConfig( binance_api_key="test_key", diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..c2538c2 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,146 @@ +"""Tests for CLI functionality.""" + +import json +import tempfile +import pytest +from pathlib import Path +from unittest.mock import patch +from click.testing import CliRunner + +from ohheycrypto.cli import main + + +class TestCLI: + """Test CLI commands.""" + + def test_version_command(self): + """Test --version command.""" + # Test using subprocess to simulate real CLI usage + import subprocess + result = subprocess.run(['python', '-m', 'ohheycrypto.cli', '--version'], + capture_output=True, text=True, cwd=Path(__file__).parent.parent) + assert result.returncode == 0 + assert 'ohheycrypto' in result.stdout.lower() + + def test_help_command(self): + """Test --help command.""" + import subprocess + result = subprocess.run(['python', '-m', 'ohheycrypto.cli', '--help'], + capture_output=True, text=True, cwd=Path(__file__).parent.parent) + assert result.returncode == 0 + assert 'OhHeyCrypto' in result.stdout + assert 'Sophisticated cryptocurrency trading bot' in result.stdout + + def test_init_command(self): + """Test init command creates config file.""" + with tempfile.TemporaryDirectory() as tmpdir: + config_path = Path(tmpdir) / "test_config.json" + + import subprocess + result = subprocess.run(['python', '-m', 'ohheycrypto.cli', 'init', str(config_path)], + capture_output=True, text=True, cwd=Path(__file__).parent.parent) + + assert result.returncode == 0 + assert config_path.exists() + + # Verify the config file is valid JSON + with open(config_path) as f: + config_data = json.load(f) + + assert 'binance' in config_data + assert 'trading' in config_data + assert 'market_analysis' in config_data + assert 'execution' in config_data + assert 'notifications' in config_data + + def test_validate_command_invalid_config(self): + """Test validate command with invalid config.""" + with tempfile.TemporaryDirectory() as tmpdir: + config_path = Path(tmpdir) / "invalid_config.json" + + # Create config with placeholder values + config_data = { + "binance": { + "api_key": "YOUR_BINANCE_API_KEY", + "api_secret": "YOUR_BINANCE_API_SECRET" + }, + "trading": { + "crypto": "BTC", + "fiat": "USDT" + } + } + + with open(config_path, 'w') as f: + json.dump(config_data, f) + + import subprocess + result = subprocess.run(['python', '-m', 'ohheycrypto.cli', 'validate', str(config_path)], + capture_output=True, text=True, cwd=Path(__file__).parent.parent) + + # Should return non-zero exit code for invalid config + assert result.returncode == 1 + assert 'API key not configured' in result.stdout or 'API key not configured' in result.stderr + + def test_validate_command_valid_config(self): + """Test validate command with valid config.""" + with tempfile.TemporaryDirectory() as tmpdir: + config_path = Path(tmpdir) / "valid_config.json" + + # Create config with valid (fake) values + config_data = { + "binance": { + "api_key": "valid_fake_api_key_12345", + "api_secret": "valid_fake_api_secret_12345" + }, + "trading": { + "crypto": "BTC", + "fiat": "USDT", + "stop_loss": 3.0, + "sell_threshold": 0.4, + "buy_threshold": 0.2, + "trailing_stop": 2.0, + "position_sizing": { + "min": 0.5, + "max": 0.95 + } + }, + "market_analysis": { + "rsi_period": 14, + "rsi_oversold": 30, + "rsi_overbought": 70, + "ma_short": 10, + "ma_long": 20, + "volatility_lookback": 20 + }, + "execution": { + "check_interval": 60, + "circuit_breaker": { + "max_failures": 5, + "cooldown": 3600 + }, + "min_order_value": 10.0 + }, + "notifications": { + "discord_webhook": "" + } + } + + with open(config_path, 'w') as f: + json.dump(config_data, f, indent=2) + + import subprocess + result = subprocess.run(['python', '-m', 'ohheycrypto.cli', 'validate', str(config_path)], + capture_output=True, text=True, cwd=Path(__file__).parent.parent) + + # Should return zero exit code for valid config + assert result.returncode == 0 + assert 'Configuration is valid' in result.stdout + + def test_run_command_help(self): + """Test run command help.""" + import subprocess + result = subprocess.run(['python', '-m', 'ohheycrypto.cli', 'run', '--help'], + capture_output=True, text=True, cwd=Path(__file__).parent.parent) + assert result.returncode == 0 + assert 'run' in result.stdout.lower() + assert '--dry' in result.stdout or '--dry-run' in result.stdout \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index d21aeec..e231188 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,7 +4,7 @@ from unittest.mock import patch, mock_open from pydantic import ValidationError -from config.settings import ( +from ohheycrypto.config.settings import ( Config, TradingConfig, MarketAnalysisConfig, ExecutionConfig, APIConfig, get_config, reload_config ) @@ -44,13 +44,17 @@ def test_validation_boundaries(self): def test_position_size_validation(self): """Test position size validation.""" # Valid - config = TradingConfig(min_position_size=0.3, max_position_size=0.7) + config = TradingConfig( + position_sizing={"min": 0.3, "max": 0.7} + ) assert config.min_position_size == 0.3 assert config.max_position_size == 0.7 # Invalid - max < min with pytest.raises(ValidationError): - TradingConfig(min_position_size=0.7, max_position_size=0.3) + TradingConfig( + position_sizing={"min": 0.7, "max": 0.3} + ) class TestMarketAnalysisConfig: @@ -68,16 +72,16 @@ def test_default_values(self): def test_ma_period_validation(self): """Test moving average period validation.""" # Valid - config = MarketAnalysisConfig(ma_short_period=15, ma_long_period=30) + config = MarketAnalysisConfig(ma_short=15, ma_long=30) assert config.ma_short_period == 15 assert config.ma_long_period == 30 # Invalid - long <= short with pytest.raises(ValidationError): - MarketAnalysisConfig(ma_short_period=20, ma_long_period=20) + MarketAnalysisConfig(ma_short=20, ma_long=20) with pytest.raises(ValidationError): - MarketAnalysisConfig(ma_short_period=25, ma_long_period=20) + MarketAnalysisConfig(ma_short=25, ma_long=20) class TestAPIConfig: @@ -145,7 +149,7 @@ def test_load_from_nonexistent_file(self, tmp_path): """Test loading from nonexistent file returns defaults.""" config_file = tmp_path / "nonexistent.json" - with patch('config.settings.logger') as mock_logger: + with patch('ohheycrypto.config.settings.logger') as mock_logger: config = Config.load_from_file(config_file) mock_logger.warning.assert_called() @@ -156,7 +160,7 @@ def test_load_from_invalid_json(self, tmp_path): config_file = tmp_path / "invalid.json" config_file.write_text("invalid json{") - with patch('config.settings.logger') as mock_logger: + with patch('ohheycrypto.config.settings.logger') as mock_logger: config = Config.load_from_file(config_file) mock_logger.error.assert_called() @@ -165,9 +169,9 @@ def test_load_from_invalid_json(self, tmp_path): def test_load_from_env(self): """Test loading configuration from environment variables.""" with patch.dict('os.environ', { - 'BOT_SL': '7.5', - 'BOT_ST': '0.8', - 'BOT_BT': '0.3', + 'BOT_SL': '0.075', # Decimal format (7.5% as 0.075) + 'BOT_ST': '0.008', # Decimal format (0.8% as 0.008) + 'BOT_BT': '0.003', # Decimal format (0.3% as 0.003) 'BOT_FIAT': 'BUSD', 'BOT_CRYPTO': 'ETH', 'BINANCE_API_KEY': 'test_api_key_123', @@ -176,9 +180,9 @@ def test_load_from_env(self): }): config = Config.load_from_env() - assert config.trading.stop_loss == 7.5 - assert config.trading.sell_threshold == 0.8 - assert config.trading.buy_threshold == 0.3 + assert config.trading.stop_loss == 7.5 # Converted to percentage + assert config.trading.sell_threshold == 0.8 # Converted to percentage + assert config.trading.buy_threshold == 0.3 # Converted to percentage assert config.trading.fiat_currency == 'BUSD' assert config.trading.crypto_currency == 'ETH' assert config.api.binance_api_key == 'test_api_key_123' @@ -196,7 +200,8 @@ def test_save_to_file(self, tmp_path): assert config_file.exists() # Load and verify - loaded_data = json.loads(config_file.read_text()) + with open(config_file) as f: + loaded_data = json.load(f) assert loaded_data['trading']['stop_loss'] == 4.5 def test_validate_for_trading(self): @@ -214,7 +219,7 @@ def test_validate_for_trading(self): class TestConfigSingleton: """Test configuration singleton behavior.""" - @patch('config.settings._config', None) + @patch('ohheycrypto.config.settings._config', None) def test_get_config_singleton(self): """Test get_config returns singleton.""" with patch.dict('os.environ', { @@ -226,7 +231,7 @@ def test_get_config_singleton(self): assert config1 is config2 - @patch('config.settings._config', None) + @patch('ohheycrypto.config.settings._config', None) def test_reload_config(self): """Test reload_config forces new instance.""" with patch.dict('os.environ', { @@ -240,7 +245,7 @@ def test_reload_config(self): assert config1 is not config2 assert config2 is config3 - @patch('config.settings._config', None) + @patch('ohheycrypto.config.settings._config', None) def test_config_with_file_and_env(self, tmp_path): """Test config loading with both file and env vars.""" # Create config file @@ -256,7 +261,7 @@ def test_config_with_file_and_env(self, tmp_path): with patch('pathlib.Path.exists', return_value=True), \ patch('builtins.open', mock_open(read_data=json.dumps(config_data))), \ patch.dict('os.environ', { - 'BOT_ST': '0.9', # Override file value + 'BOT_ST': '0.009', # Override file value (0.9% as decimal) 'BINANCE_API_KEY': 'env_key', 'BINANCE_API_SECRET': 'env_secret' }): @@ -265,7 +270,7 @@ def test_config_with_file_and_env(self, tmp_path): # File value assert config.trading.stop_loss == 5.0 - # Env override - assert config.trading.sell_threshold == 0.9 + # Env override (check for floating point precision) + assert abs(config.trading.sell_threshold - 0.9) < 0.0001 # Env value assert config.api.binance_api_key == 'env_key' \ No newline at end of file diff --git a/tests/test_discord.py b/tests/test_discord.py index f6ef668..7e0c714 100644 --- a/tests/test_discord.py +++ b/tests/test_discord.py @@ -3,7 +3,7 @@ import requests from requests.exceptions import RequestException, Timeout, ConnectionError -from plugins.discord import DiscordService +from ohheycrypto.plugins.discord import DiscordService class TestDiscordService: @@ -28,7 +28,7 @@ def test_discord_initialization_invalid_webhook(self): with patch.dict('os.environ', { 'DISCORD_WEBHOOK': 'https://invalid-webhook.com/webhook' }): - with patch('plugins.discord.logger') as mock_logger: + with patch('ohheycrypto.plugins.discord.logger') as mock_logger: service = DiscordService() mock_logger.warning.assert_called_with("Invalid Discord webhook URL format") @@ -75,7 +75,7 @@ def test_post_missing_fields(self): # Missing content and embeds } - with patch('plugins.discord.logger') as mock_logger: + with patch('ohheycrypto.plugins.discord.logger') as mock_logger: result = service.post("https://discord.com/api/webhooks/test", payload) assert result is None mock_logger.error.assert_called() @@ -96,8 +96,8 @@ def test_post_rate_limited(self, mock_post): "embeds": [] } - with pytest.raises(RequestException): - service.post("https://discord.com/api/webhooks/test", payload) + result = service.post("https://discord.com/api/webhooks/test", payload) + assert result is None # Should return None for client errors like 429 @patch('requests.post') def test_post_webhook_not_found(self, mock_post): @@ -138,7 +138,7 @@ def test_post_with_retry(self, mock_post): assert result == mock_response assert mock_post.call_count == 2 - @patch('plugins.discord.DiscordService.post') + @patch('ohheycrypto.plugins.discord.DiscordService.post') def test_sell_notification(self, mock_post): """Test sell order notification.""" with patch.dict('os.environ', { @@ -161,7 +161,7 @@ def test_sell_notification(self, mock_post): assert call_args['channel_url'] == 'https://discord.com/api/webhooks/test' assert call_args['payload']['content'] == "SELL order created" - @patch('plugins.discord.DiscordService.post') + @patch('ohheycrypto.plugins.discord.DiscordService.post') def test_buy_notification(self, mock_post): """Test buy order notification.""" with patch.dict('os.environ', { @@ -184,7 +184,7 @@ def test_buy_notification(self, mock_post): assert call_args['channel_url'] == 'https://discord.com/api/webhooks/test' assert call_args['payload']['content'] == "BUY order created" - @patch('plugins.discord.DiscordService.post') + @patch('ohheycrypto.plugins.discord.DiscordService.post') def test_bot_online_notification(self, mock_post): """Test bot online notification.""" with patch.dict('os.environ', { @@ -216,7 +216,7 @@ def test_notification_without_webhook(self): service.buy({"symbol": "BTCUSDT"}) service.botOnline() - @patch('plugins.discord.DiscordService.post') + @patch('ohheycrypto.plugins.discord.DiscordService.post') def test_notification_error_handling(self, mock_post): """Test error handling in notifications.""" mock_post.side_effect = Exception("Network error") diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 2b03da7..42093b6 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -2,13 +2,13 @@ from unittest.mock import Mock, patch, MagicMock from binance.exceptions import BinanceAPIException -from services.wallet import Wallet +from ohheycrypto.services.wallet import Wallet class TestWallet: """Test cases for Wallet service.""" - @patch('services.wallet.Client') + @patch('ohheycrypto.services.wallet.Client') def test_wallet_initialization(self, mock_client_class, mock_binance_client): """Test wallet initialization with successful balance sync.""" mock_client_class.return_value = mock_binance_client @@ -22,11 +22,11 @@ def test_wallet_initialization(self, mock_client_class, mock_binance_client): assert wallet._balances == {"USDT": 1000.0, "BTC": 0.5} mock_binance_client.get_account.assert_called_once() - @patch('services.wallet.Client') + @patch('ohheycrypto.services.wallet.Client') def test_wallet_initialization_failure(self, mock_client_class): """Test wallet initialization when API fails.""" mock_client = Mock() - mock_client.get_account.side_effect = BinanceAPIException(Mock(), Mock(), {"code": -1000, "msg": "Unknown error"}) + mock_client.get_account.side_effect = BinanceAPIException(Mock(), 400, '{"code": -1000, "msg": "Unknown error"}') mock_client_class.return_value = mock_client with patch.dict('os.environ', { @@ -38,7 +38,7 @@ def test_wallet_initialization_failure(self, mock_client_class): # Should have empty balances on failure assert wallet._balances == {} - @patch('services.wallet.Client') + @patch('ohheycrypto.services.wallet.Client') def test_sync_success(self, mock_client_class, mock_binance_client): """Test successful balance sync.""" mock_client_class.return_value = mock_binance_client @@ -63,20 +63,27 @@ def test_sync_success(self, mock_client_class, mock_binance_client): assert new_balances == {"USDT": 2000.0, "BTC": 1.0, "ETH": 5.0} assert wallet._balances == {"USDT": 2000.0, "BTC": 1.0, "ETH": 5.0} - @patch('services.wallet.Client') + @patch('ohheycrypto.services.wallet.Client') def test_sync_with_retry(self, mock_client_class): """Test sync with retry on API failure.""" mock_client = Mock() - # First call fails, second succeeds - mock_client.get_account.side_effect = [ - BinanceAPIException(Mock(), Mock(), {"code": -1003, "msg": "Too many requests"}), + + # Create a separate mock for initialization (succeeds) + init_mock = Mock() + init_mock.get_account.return_value = {"balances": []} + + # Create sync mock that fails first, then succeeds + sync_mock = Mock() + sync_mock.get_account.side_effect = [ + BinanceAPIException(Mock(), 429, '{"code": -1003, "msg": "Too many requests"}'), { "balances": [ {"asset": "USDT", "free": "1500.0", "locked": "0.0"}, ] } ] - mock_client_class.return_value = mock_client + + mock_client_class.side_effect = [init_mock, sync_mock] with patch.dict('os.environ', { 'BINANCE_API_KEY': 'test_key', @@ -85,13 +92,16 @@ def test_sync_with_retry(self, mock_client_class): wallet = Wallet() wallet._balances = {} # Reset balances + # Replace the client for sync operation + wallet.client = sync_mock + with patch('time.sleep'): # Mock sleep to speed up test balances = wallet.sync() assert balances == {"USDT": 1500.0} - assert mock_client.get_account.call_count >= 2 + assert sync_mock.get_account.call_count == 2 - @patch('services.wallet.Client') + @patch('ohheycrypto.services.wallet.Client') def test_has_balance(self, mock_client_class, mock_binance_client): """Test checking if wallet has balance for a symbol.""" mock_client_class.return_value = mock_binance_client @@ -107,7 +117,7 @@ def test_has_balance(self, mock_client_class, mock_binance_client): assert wallet.has("ETH") is False assert wallet.has("DOGE") is False - @patch('services.wallet.Client') + @patch('ohheycrypto.services.wallet.Client') def test_balance(self, mock_client_class, mock_binance_client): """Test getting balance for specific symbol.""" mock_client_class.return_value = mock_binance_client @@ -123,7 +133,7 @@ def test_balance(self, mock_client_class, mock_binance_client): assert wallet.balance("ETH") == 0 assert wallet.balance("INVALID") == 0 - @patch('services.wallet.Client') + @patch('ohheycrypto.services.wallet.Client') def test_balances(self, mock_client_class, mock_binance_client): """Test getting all balances.""" mock_client_class.return_value = mock_binance_client @@ -141,7 +151,7 @@ def test_balances(self, mock_client_class, mock_binance_client): balances["USDT"] = 0 assert wallet._balances["USDT"] == 1000.0 - @patch('services.wallet.Client') + @patch('ohheycrypto.services.wallet.Client') def test_refresh_balances(self, mock_client_class, mock_binance_client): """Test force refresh of balances.""" mock_client_class.return_value = mock_binance_client @@ -163,7 +173,7 @@ def test_refresh_balances(self, mock_client_class, mock_binance_client): assert refreshed == {"USDT": 3000.0} assert wallet._balances == {"USDT": 3000.0} - @patch('services.wallet.Client') + @patch('ohheycrypto.services.wallet.Client') def test_refresh_balances_failure(self, mock_client_class): """Test refresh balances when API fails.""" mock_client = Mock() From 58f6e9131096476552b1edd84b0b69e6956f730c Mon Sep 17 00:00:00 2001 From: Sean N Date: Mon, 11 Aug 2025 13:36:44 +0200 Subject: [PATCH 2/3] Fixed the logging and configs --- README.md | 2 +- ohheycrypto/cli.py | 2 +- ohheycrypto/config/settings.py | 3 --- ohheycrypto/services/logging.py | 4 ++-- setup.py | 8 ++++---- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cd427ae..df0ebb3 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ ohheycrypto run production_config.json ### Development/Source Installation: ```bash -git clone https://github.com/ohheycrypto/bot +git clone https://github.com/sn/ohheycrypto cd bot pip install -e . ohheycrypto init dev_config.json diff --git a/ohheycrypto/cli.py b/ohheycrypto/cli.py index 77652d1..4268d4e 100644 --- a/ohheycrypto/cli.py +++ b/ohheycrypto/cli.py @@ -226,7 +226,7 @@ def main(): ohheycrypto run config.json # Run the trading bot ohheycrypto run config.json --dry # Dry run (analysis only, no trades) -For more information, visit: https://github.com/ohheycrypto/bot +For more information, visit: https://github.com/sn/ohheycrypto """ ) diff --git a/ohheycrypto/config/settings.py b/ohheycrypto/config/settings.py index 95eab37..a06795b 100644 --- a/ohheycrypto/config/settings.py +++ b/ohheycrypto/config/settings.py @@ -151,7 +151,6 @@ def validate_discord_webhook(cls, v): class Config(BaseModel): """Main configuration class.""" - # New structure for JSON config files binance: Optional[BinanceConfig] = Field(default=None) trading: TradingConfig = Field(default_factory=TradingConfig) market_analysis: MarketAnalysisConfig = Field(default_factory=MarketAnalysisConfig) @@ -163,7 +162,6 @@ class Config(BaseModel): def __init__(self, **data): super().__init__(**data) - # Map new structure to legacy API config if self.binance: self.api.binance_api_key = self.binance.api_key self.api.binance_api_secret = self.binance.api_secret @@ -245,7 +243,6 @@ def get_config() -> Config: """Get the configuration singleton.""" global _config if _config is None: - # Try loading from file first config_path = Path("config.json") if config_path.exists(): _config = Config.load_from_file(config_path) diff --git a/ohheycrypto/services/logging.py b/ohheycrypto/services/logging.py index 76111a7..79b7318 100644 --- a/ohheycrypto/services/logging.py +++ b/ohheycrypto/services/logging.py @@ -67,8 +67,8 @@ def print_banner(): print(chalk.green_bright("╚══╩╝╚╩╝╚╩══╩═╗╔╩══╩╝╚═╗╔╣╔═╩═╩══╝")) print(chalk.green_bright("────────────╔═╝║─────╔═╝║║║")) print(chalk.green_bright("────────────╚══╝─────╚══╝╚╝")) - print("Trading Bot v0.1.0") - print("https://github.com/ohheycrypto/bot") + print("Trading Bot v0.2.0") + print("https://github.com/sn/ohheycrypto") print("\n") LoggingService.success("Bot started.") diff --git a/setup.py b/setup.py index 3d18287..00a7a25 100644 --- a/setup.py +++ b/setup.py @@ -24,11 +24,11 @@ description="A sophisticated cryptocurrency trading bot with advanced technical analysis and risk management", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/ohheycrypto/bot", + url="https://github.com/sn/ohheycrypto", project_urls={ - "Bug Tracker": "https://github.com/ohheycrypto/bot/issues", - "Documentation": "https://github.com/ohheycrypto/bot#readme", - "Source Code": "https://github.com/ohheycrypto/bot", + "Bug Tracker": "https://github.com/sn/ohheycrypto/issues", + "Documentation": "https://github.com/sn/ohheycrypto#readme", + "Source Code": "https://github.com/sn/ohheycrypto", }, packages=find_packages(exclude=["tests", "tests.*", "docs", "docs.*"]), classifiers=[ From 701ff240f8dd4295eb66b9224c7c39141b6dbd36 Mon Sep 17 00:00:00 2001 From: Sean N Date: Mon, 11 Aug 2025 13:42:24 +0200 Subject: [PATCH 3/3] Updated the README --- README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index df0ebb3..39a5ef8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ This is a **crypto trading bot** that uses sophisticated technical analysis and ![Trading Bot Screenshot](static/screenshot.png) +### Install: + +```shell +pip install ohheycrypto +``` + ## Features ### Core Trading Features @@ -24,15 +30,7 @@ This is a **crypto trading bot** that uses sophisticated technical analysis and - **Technical Indicators**: RSI-based entry/exit signals for better timing - **Comprehensive Logging**: Detailed market conditions and trading decisions -## Installation - -### Install from PyPI (recommended): - -```shell -pip install ohheycrypto -``` - -### Quick Start: +## Quick Start: ```shell # Create a configuration file