feat(v0.1.0): project foundation with logging and config
This commit is contained in:
2
tests/__init__.py
Normal file
2
tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Test suite for ICT ML Trading System."""
|
||||
|
||||
135
tests/conftest.py
Normal file
135
tests/conftest.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""Pytest configuration and fixtures."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.constants import PATHS
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir() -> Generator[Path, None, None]:
|
||||
"""
|
||||
Create a temporary directory for tests.
|
||||
|
||||
Yields:
|
||||
Path to temporary directory
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
yield Path(tmpdir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_config_dir(temp_dir: Path) -> Generator[Path, None, None]:
|
||||
"""
|
||||
Create a temporary config directory with minimal config files.
|
||||
|
||||
Yields:
|
||||
Path to temporary config directory
|
||||
"""
|
||||
config_dir = temp_dir / "config"
|
||||
config_dir.mkdir()
|
||||
|
||||
# Create minimal config.yaml
|
||||
config_file = config_dir / "config.yaml"
|
||||
config_file.write_text(
|
||||
"""
|
||||
app:
|
||||
name: "Test App"
|
||||
version: "0.1.0"
|
||||
environment: "test"
|
||||
debug: true
|
||||
|
||||
trading:
|
||||
session:
|
||||
start_time: "03:00"
|
||||
end_time: "04:00"
|
||||
timezone: "America/New_York"
|
||||
instrument:
|
||||
symbol: "TEST"
|
||||
exchange: "TEST"
|
||||
contract_size: 25
|
||||
|
||||
data:
|
||||
raw_data_path: "data/raw"
|
||||
processed_data_path: "data/processed"
|
||||
labels_path: "data/labels"
|
||||
screenshots_path: "data/screenshots"
|
||||
timeframes:
|
||||
- "1min"
|
||||
- "5min"
|
||||
- "15min"
|
||||
|
||||
models:
|
||||
base_path: "models"
|
||||
pattern_graders_path: "models/pattern_graders"
|
||||
strategy_models_path: "models/strategy_models"
|
||||
min_labels_per_pattern: 200
|
||||
train_test_split: 0.8
|
||||
"""
|
||||
)
|
||||
|
||||
# Create minimal logging.yaml
|
||||
logging_file = config_dir / "logging.yaml"
|
||||
logging_file.write_text(
|
||||
"""
|
||||
version: 1
|
||||
disable_existing_loggers: false
|
||||
|
||||
formatters:
|
||||
detailed:
|
||||
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
datefmt: '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
handlers:
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
level: INFO
|
||||
formatter: detailed
|
||||
stream: ext://sys.stdout
|
||||
|
||||
loggers:
|
||||
src:
|
||||
level: DEBUG
|
||||
handlers:
|
||||
- console
|
||||
propagate: false
|
||||
|
||||
root:
|
||||
level: INFO
|
||||
handlers:
|
||||
- console
|
||||
"""
|
||||
)
|
||||
|
||||
yield config_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_ohlcv_data():
|
||||
"""Sample OHLCV data for testing."""
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
dates = [datetime(2024, 1, 1, 3, 0) + timedelta(minutes=i) for i in range(60)]
|
||||
data = pd.DataFrame(
|
||||
{
|
||||
"timestamp": dates,
|
||||
"open": [100.0 + i * 0.1 for i in range(60)],
|
||||
"high": [100.5 + i * 0.1 for i in range(60)],
|
||||
"low": [99.5 + i * 0.1 for i in range(60)],
|
||||
"close": [100.2 + i * 0.1 for i in range(60)],
|
||||
"volume": [1000] * 60,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_config():
|
||||
"""Reset global config cache before each test."""
|
||||
import src.config.config_loader as config_module
|
||||
config_module._config = None
|
||||
|
||||
104
tests/unit/test_config/test_config_loader.py
Normal file
104
tests/unit/test_config/test_config_loader.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""Tests for configuration loader."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.config.config_loader import get_config, load_config
|
||||
from src.core.exceptions import ConfigurationError
|
||||
|
||||
|
||||
def test_load_config_success(temp_config_dir, monkeypatch):
|
||||
"""Test successful config loading."""
|
||||
from src.core import constants
|
||||
original_path = constants.PATHS["config"]
|
||||
constants.PATHS["config"] = temp_config_dir
|
||||
|
||||
config = load_config()
|
||||
assert config is not None
|
||||
assert "app" in config
|
||||
assert config["app"]["name"] == "Test App"
|
||||
|
||||
constants.PATHS["config"] = original_path
|
||||
|
||||
|
||||
def test_load_config_missing_file(temp_dir, monkeypatch):
|
||||
"""Test config loading with missing file."""
|
||||
from src.core import constants
|
||||
original_path = constants.PATHS["config"]
|
||||
constants.PATHS["config"] = temp_dir / "nonexistent"
|
||||
|
||||
with pytest.raises(ConfigurationError):
|
||||
load_config()
|
||||
|
||||
constants.PATHS["config"] = original_path
|
||||
|
||||
|
||||
def test_get_config_with_key(temp_config_dir, monkeypatch):
|
||||
"""Test getting config value by key."""
|
||||
from src.core import constants
|
||||
original_path = constants.PATHS["config"]
|
||||
constants.PATHS["config"] = temp_config_dir
|
||||
|
||||
value = get_config("app.name")
|
||||
assert value == "Test App"
|
||||
|
||||
constants.PATHS["config"] = original_path
|
||||
|
||||
|
||||
def test_get_config_with_default(temp_config_dir, monkeypatch):
|
||||
"""Test getting config with default value."""
|
||||
from src.core import constants
|
||||
original_path = constants.PATHS["config"]
|
||||
constants.PATHS["config"] = temp_config_dir
|
||||
|
||||
value = get_config("nonexistent.key", default="default_value")
|
||||
assert value == "default_value"
|
||||
|
||||
constants.PATHS["config"] = original_path
|
||||
|
||||
|
||||
def test_get_config_none(temp_config_dir, monkeypatch):
|
||||
"""Test getting entire config."""
|
||||
from src.core import constants
|
||||
original_path = constants.PATHS["config"]
|
||||
constants.PATHS["config"] = temp_config_dir
|
||||
|
||||
config = get_config()
|
||||
assert isinstance(config, dict)
|
||||
assert "app" in config
|
||||
|
||||
constants.PATHS["config"] = original_path
|
||||
|
||||
|
||||
def test_env_var_substitution(temp_config_dir, monkeypatch):
|
||||
"""Test environment variable substitution in config."""
|
||||
from src.core import constants
|
||||
original_path = constants.PATHS["config"]
|
||||
constants.PATHS["config"] = temp_config_dir
|
||||
|
||||
# Set environment variable
|
||||
os.environ["TEST_VAR"] = "test_value"
|
||||
|
||||
# Create config with env var
|
||||
config_file = temp_config_dir / "config.yaml"
|
||||
config_file.write_text(
|
||||
"""
|
||||
app:
|
||||
name: "${TEST_VAR}"
|
||||
version: "0.1.0"
|
||||
"""
|
||||
)
|
||||
|
||||
# Reset config cache
|
||||
import src.config.config_loader as config_module
|
||||
config_module._config = None
|
||||
|
||||
config = load_config()
|
||||
assert config["app"]["name"] == "test_value"
|
||||
|
||||
# Cleanup
|
||||
del os.environ["TEST_VAR"]
|
||||
constants.PATHS["config"] = original_path
|
||||
|
||||
90
tests/unit/test_core/test_exceptions.py
Normal file
90
tests/unit/test_core/test_exceptions.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Tests for custom exception classes."""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.exceptions import (
|
||||
ConfigurationError,
|
||||
DataError,
|
||||
DetectorError,
|
||||
ICTTradingException,
|
||||
ModelError,
|
||||
TradingError,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
|
||||
def test_base_exception():
|
||||
"""Test base exception class."""
|
||||
exc = ICTTradingException("Test message")
|
||||
assert str(exc) == "Test message"
|
||||
assert exc.message == "Test message"
|
||||
assert exc.error_code is None
|
||||
assert exc.context == {}
|
||||
|
||||
|
||||
def test_exception_with_error_code():
|
||||
"""Test exception with error code."""
|
||||
exc = ICTTradingException("Test message", error_code="TEST_ERROR")
|
||||
assert str(exc) == "[TEST_ERROR] Test message"
|
||||
assert exc.error_code == "TEST_ERROR"
|
||||
|
||||
|
||||
def test_exception_with_context():
|
||||
"""Test exception with context."""
|
||||
context = {"key": "value", "number": 42}
|
||||
exc = ICTTradingException("Test message", context=context)
|
||||
assert exc.context == context
|
||||
assert exc.to_dict()["context"] == context
|
||||
|
||||
|
||||
def test_exception_to_dict():
|
||||
"""Test exception to_dict method."""
|
||||
exc = ICTTradingException("Test message", error_code="TEST", context={"key": "value"})
|
||||
exc_dict = exc.to_dict()
|
||||
assert exc_dict["error_type"] == "ICTTradingException"
|
||||
assert exc_dict["error_code"] == "TEST"
|
||||
assert exc_dict["message"] == "Test message"
|
||||
assert exc_dict["context"] == {"key": "value"}
|
||||
|
||||
|
||||
def test_data_error():
|
||||
"""Test DataError exception."""
|
||||
exc = DataError("Data loading failed")
|
||||
assert isinstance(exc, ICTTradingException)
|
||||
assert exc.error_code == "DATA_ERROR"
|
||||
|
||||
|
||||
def test_detector_error():
|
||||
"""Test DetectorError exception."""
|
||||
exc = DetectorError("Detection failed")
|
||||
assert isinstance(exc, ICTTradingException)
|
||||
assert exc.error_code == "DETECTOR_ERROR"
|
||||
|
||||
|
||||
def test_model_error():
|
||||
"""Test ModelError exception."""
|
||||
exc = ModelError("Model training failed")
|
||||
assert isinstance(exc, ICTTradingException)
|
||||
assert exc.error_code == "MODEL_ERROR"
|
||||
|
||||
|
||||
def test_configuration_error():
|
||||
"""Test ConfigurationError exception."""
|
||||
exc = ConfigurationError("Invalid config")
|
||||
assert isinstance(exc, ICTTradingException)
|
||||
assert exc.error_code == "CONFIG_ERROR"
|
||||
|
||||
|
||||
def test_trading_error():
|
||||
"""Test TradingError exception."""
|
||||
exc = TradingError("Trade execution failed")
|
||||
assert isinstance(exc, ICTTradingException)
|
||||
assert exc.error_code == "TRADING_ERROR"
|
||||
|
||||
|
||||
def test_validation_error():
|
||||
"""Test ValidationError exception."""
|
||||
exc = ValidationError("Validation failed")
|
||||
assert isinstance(exc, ICTTradingException)
|
||||
assert exc.error_code == "VALIDATION_ERROR"
|
||||
|
||||
83
tests/unit/test_logging/test_logger.py
Normal file
83
tests/unit/test_logging/test_logger.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Tests for logging system."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.exceptions import ConfigurationError
|
||||
from src.logging import get_logger
|
||||
|
||||
|
||||
def test_get_logger_with_name():
|
||||
"""Test getting logger with name."""
|
||||
logger = get_logger("test_module")
|
||||
assert isinstance(logger, logging.Logger)
|
||||
assert logger.name == "test_module"
|
||||
|
||||
|
||||
def test_get_logger_root():
|
||||
"""Test getting root logger."""
|
||||
logger = get_logger()
|
||||
assert isinstance(logger, logging.Logger)
|
||||
|
||||
|
||||
def test_logger_logs_message(caplog):
|
||||
"""Test that logger actually logs messages."""
|
||||
logger = get_logger("test")
|
||||
logger.info("Test message")
|
||||
assert "Test message" in caplog.text
|
||||
|
||||
|
||||
def test_logger_with_missing_config(temp_dir, monkeypatch):
|
||||
"""Test logger with missing config file."""
|
||||
# Temporarily change config path to non-existent location
|
||||
from src.core import constants
|
||||
original_path = constants.PATHS["config"]
|
||||
constants.PATHS["config"] = temp_dir / "nonexistent"
|
||||
|
||||
with pytest.raises(ConfigurationError):
|
||||
get_logger("test")
|
||||
|
||||
# Restore original path
|
||||
constants.PATHS["config"] = original_path
|
||||
|
||||
|
||||
def test_logger_creates_directories(temp_dir, monkeypatch):
|
||||
"""Test that logger creates log directories."""
|
||||
from src.core import constants
|
||||
original_path = constants.PATHS["logs"]
|
||||
constants.PATHS["logs"] = temp_dir / "logs"
|
||||
|
||||
# Create minimal config
|
||||
config_dir = temp_dir / "config"
|
||||
config_dir.mkdir()
|
||||
logging_config = config_dir / "logging.yaml"
|
||||
logging_config.write_text(
|
||||
"""
|
||||
version: 1
|
||||
disable_existing_loggers: false
|
||||
|
||||
formatters:
|
||||
detailed:
|
||||
format: '%(message)s'
|
||||
|
||||
handlers:
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
level: INFO
|
||||
formatter: detailed
|
||||
|
||||
root:
|
||||
level: INFO
|
||||
handlers:
|
||||
- console
|
||||
"""
|
||||
)
|
||||
|
||||
logger = get_logger("test")
|
||||
assert isinstance(logger, logging.Logger)
|
||||
|
||||
# Restore original path
|
||||
constants.PATHS["logs"] = original_path
|
||||
|
||||
Reference in New Issue
Block a user