Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,56 @@ venv.bak/

.DS_STORE
.idea/
output/data.js
output/data.js

# Claude settings
.claude/*

# Virtual environments (additional)
virtualenv/
*/.venv/*
*/venv/*

# IDE files
.vscode/
*.swp
*.swo
*~

# Testing artifacts
.pytest_cache/
.coverage
.coverage.*
htmlcov/
coverage.xml
*.cover
.hypothesis/
test-results/
pytest_cache/

# Build artifacts
*.pyc
*.pyo
*.pyd
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg

# Package manager files (DO NOT ignore lock files)
# poetry.lock # Keep this file in version control
# uv.lock # Keep this file in version control
1,551 changes: 1,551 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
[tool.poetry]
name = "trading-bot"
version = "0.1.0"
description = "A cryptocurrency trading bot"
authors = ["Your Name <you@example.com>"]
readme = "README.md"
packages = [{include = "."}]

[tool.poetry.dependencies]
python = "^3.11"
ccxt = "^4.0.0"
pandas = "^2.0.0"
python-dotenv = "^1.0.0"

[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
pytest-cov = "^5.0.0"
pytest-mock = "^3.14.0"

# Scripts can be run with: poetry run pytest
# No custom scripts defined - use poetry run pytest directly

[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",
"--strict-config",
"--cov=.",
"--cov-branch",
"--cov-report=term-missing:skip-covered",
"--cov-report=html:htmlcov",
"--cov-report=xml:coverage.xml",
"--cov-fail-under=10",
"-vv"
]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests"
]
filterwarnings = [
"error",
"ignore::UserWarning",
"ignore::DeprecationWarning"
]

[tool.coverage.run]
source = ["."]
omit = [
"*/tests/*",
"*/__pycache__/*",
"*/venv/*",
"*/.venv/*",
"*/virtualenv/*",
"*/output/*",
"*/htmlcov/*",
"setup.py",
"*/conftest.py"
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"def __str__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:"
]
precision = 2
show_missing = true
skip_covered = false
fail_under = 10

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Empty file added tests/__init__.py
Empty file.
217 changes: 217 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import os
import sys
import tempfile
from pathlib import Path
from unittest.mock import Mock, MagicMock
import pytest
import json
from datetime import datetime

# Add the project root to the Python path
sys.path.insert(0, str(Path(__file__).parent.parent))


@pytest.fixture
def temp_dir():
"""Create a temporary directory for test files."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)


@pytest.fixture
def mock_config():
"""Provide a mock configuration dictionary."""
return {
"api_key": "test_api_key",
"api_secret": "test_api_secret",
"exchange": "binance",
"symbol": "BTC/USDT",
"timeframe": "1h",
"live": False,
"test_mode": True
}


@pytest.fixture
def mock_api_response():
"""Mock API response data."""
return {
"symbol": "BTC/USDT",
"price": 50000.0,
"volume": 1234.56,
"timestamp": datetime.now().timestamp(),
"bid": 49999.0,
"ask": 50001.0
}


@pytest.fixture
def mock_candlestick_data():
"""Mock candlestick/OHLCV data."""
return [
[1609459200000, 28923.63, 29000.00, 28700.00, 28950.00, 234.56],
[1609462800000, 28950.00, 29100.00, 28900.00, 29050.00, 245.67],
[1609466400000, 29050.00, 29200.00, 29000.00, 29150.00, 256.78],
[1609470000000, 29150.00, 29300.00, 29100.00, 29250.00, 267.89],
[1609473600000, 29250.00, 29400.00, 29200.00, 29350.00, 278.90]
]


@pytest.fixture
def mock_exchange():
"""Create a mock exchange object."""
exchange = Mock()
exchange.fetch_ticker = Mock(return_value={
"symbol": "BTC/USDT",
"last": 50000.0,
"bid": 49999.0,
"ask": 50001.0,
"volume": 1234.56
})
exchange.fetch_ohlcv = Mock(return_value=[
[1609459200000, 28923.63, 29000.00, 28700.00, 28950.00, 234.56]
])
exchange.create_order = Mock(return_value={
"id": "12345",
"symbol": "BTC/USDT",
"type": "limit",
"side": "buy",
"price": 50000.0,
"amount": 0.01,
"status": "open"
})
return exchange


@pytest.fixture
def mock_logger():
"""Create a mock logger."""
logger = Mock()
logger.info = Mock()
logger.error = Mock()
logger.warning = Mock()
logger.debug = Mock()
return logger


@pytest.fixture
def sample_json_file(temp_dir):
"""Create a sample JSON file for testing."""
json_path = temp_dir / "test_data.json"
data = {
"test": "data",
"number": 42,
"list": [1, 2, 3],
"nested": {"key": "value"}
}
with open(json_path, 'w') as f:
json.dump(data, f)
return json_path


@pytest.fixture
def mock_env_vars(monkeypatch):
"""Set up mock environment variables."""
env_vars = {
"API_KEY": "test_api_key",
"API_SECRET": "test_api_secret",
"EXCHANGE": "binance",
"TRADING_SYMBOL": "BTC/USDT"
}
for key, value in env_vars.items():
monkeypatch.setenv(key, value)
return env_vars


@pytest.fixture(autouse=True)
def reset_shared_module(monkeypatch):
"""Reset the shared module state before each test."""
# Set default environment variables for testing
test_env_vars = {
"ASSET": "BTC",
"MARKET": "USDT",
"COINS_MARKET": "1000.0",
"COINS_ASSET": "0.1",
"SPREAD": "0.001",
"FEES": "0.001",
"INTERVAL": "60",
"EXCHANGE": "binance",
"TIMEFRAME": "1h",
"START_DATE": "2021-01-01",
"ALLOCATION": "0.1"
}

for key, value in test_env_vars.items():
if key not in os.environ:
monkeypatch.setenv(key, value)

# Now import shared after environment is set up
import shared
# Reset any global state in the shared module
if hasattr(shared, 'exchange'):
original_exchange = shared.exchange.copy() if hasattr(shared.exchange, 'copy') else None
yield
# Cleanup after test if needed
if hasattr(shared, 'exchange') and original_exchange:
shared.exchange = original_exchange


@pytest.fixture
def mock_bot_api():
"""Create a mock BotApi instance."""
from unittest.mock import MagicMock
bot_api = MagicMock()
bot_api.getTicker = Mock(return_value={"last": 50000.0})
bot_api.getBalance = Mock(return_value={"BTC": 1.0, "USDT": 10000.0})
bot_api.buy = Mock(return_value={"id": "buy_123"})
bot_api.sell = Mock(return_value={"id": "sell_123"})
return bot_api


@pytest.fixture
def mock_bot_log():
"""Create a mock BotLog instance."""
from unittest.mock import MagicMock
bot_log = MagicMock()
bot_log.info = Mock()
bot_log.error = Mock()
bot_log.warning = Mock()
return bot_log


@pytest.fixture
def capture_stdout(monkeypatch):
"""Capture stdout for testing print statements."""
import io
captured_output = io.StringIO()
monkeypatch.setattr('sys.stdout', captured_output)
return captured_output


@pytest.fixture
def mock_time(monkeypatch):
"""Mock time.time() for consistent testing."""
current_time = 1609459200.0 # 2021-01-01 00:00:00 UTC

def mock_time_func():
nonlocal current_time
current_time += 1
return current_time

monkeypatch.setattr('time.time', mock_time_func)
return mock_time_func


@pytest.fixture
def pandas_dataframe():
"""Create a sample pandas DataFrame for testing."""
import pandas as pd
data = {
'timestamp': [1609459200, 1609462800, 1609466400],
'open': [28923.63, 28950.00, 29050.00],
'high': [29000.00, 29100.00, 29200.00],
'low': [28700.00, 28900.00, 29000.00],
'close': [28950.00, 29050.00, 29150.00],
'volume': [234.56, 245.67, 256.78]
}
return pd.DataFrame(data)
Empty file added tests/integration/__init__.py
Empty file.
Loading