diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..034bf46e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,114 @@ +name: CI + +on: + push: + branches: [main, master, develop, gamma, beta, "feature/*", "claude/*"] + pull_request: + branches: [main, master, develop, gamma, beta] + +jobs: + lint: + name: Lint & Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff black + + - name: Check formatting with Black + run: black --check --diff . + + - name: Lint with Ruff + run: ruff check . + + type-check: + name: Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run mypy + run: mypy quantcoder --ignore-missing-imports + + test: + name: Test (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + pip install pytest-cov pytest-mock + python -m spacy download en_core_web_sm + + - name: Run tests + run: pytest tests/ -v --cov=quantcoder --cov-report=xml + + - name: Upload coverage + uses: codecov/codecov-action@v3 + if: matrix.python-version == '3.11' + with: + files: ./coverage.xml + fail_ci_if_error: false + + security: + name: Security Scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pip-audit + + - name: Run pip-audit + run: pip-audit --require-hashes=false || true + + secret-scan: + name: Secret Scanning + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: TruffleHog Secret Scan + uses: trufflesecurity/trufflehog@main + with: + extra_args: --only-verified diff --git a/.gitignore b/.gitignore index 05a93f86..160848ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ -# Python +# Python bytecode and cache __pycache__/ *.py[cod] *$py.class *.so .Python + +# Distribution / packaging build/ develop-eggs/ dist/ @@ -20,6 +22,7 @@ wheels/ .installed.cfg *.egg MANIFEST +*.whl # Virtual Environment .venv/ @@ -27,40 +30,66 @@ MANIFEST venv/ ENV/ env/ +.env/ -# IDE +# IDE and editors .vscode/ .idea/ *.swp *.swo *~ +.project +.pydevproject +.settings/ -# Logs +# Logs and output artifacts *.log +logs/ quantcli.log article_processor.log -# QuantCoder specific +# QuantCoder specific - user data downloads/ generated_code/ articles.json output.html +output.* -# Configuration (contains API keys) +# Configuration and secrets (API keys) .env +.env.* +*.env +.envrc .quantcoder/ +secrets.json +credentials.json -# OS +# OS specific .DS_Store Thumbs.db -# SpaCy models +# SpaCy models (large binary files) *.bin -# Testing +# Testing and coverage .pytest_cache/ .coverage +.coverage.* htmlcov/ +coverage.xml +*.cover +.hypothesis/ +.tox/ +.nox/ -# Distribution -*.whl +# Type checking +.mypy_cache/ +.dmypy.json +dmypy.json +.pytype/ + +# Jupyter +.ipynb_checkpoints/ + +# Local development +*.local diff --git a/article_processor.log b/article_processor.log deleted file mode 100644 index 2a71810d..00000000 --- a/article_processor.log +++ /dev/null @@ -1 +0,0 @@ -2024-10-10 15:05:25,643 - INFO - quantcli.cli - Searching for articles with query: breakout detection stocks, number of results: 5 diff --git a/articles.json b/articles.json deleted file mode 100644 index 3b388afc..00000000 --- a/articles.json +++ /dev/null @@ -1,47 +0,0 @@ -[ - { - "id": "1", - "title": "Trading Range Breakout Test on Daily Stocks of Indian Markets", - "authors": "Uttam B Sapate", - "published": null, - "URL": "http://dx.doi.org/10.2139/ssrn.3068852", - "DOI": "10.2139/ssrn.3068852", - "abstract": "No abstract available." - }, - { - "id": "2", - "title": "Breakout Stocks Identification using Machine Learning Approaches", - "authors": "Md. Siam Ansary", - "published": 2022, - "URL": "http://dx.doi.org/10.53907/enpesj.v2i2.173", - "DOI": "10.53907/enpesj.v2i2.173", - "abstract": "Stock market offers a platform for people to engage in trading. It contributes to the growth of nation. Decision making regarding investments needs to be done very carefully so that an investor does not suffer massive loss. Since the share market is susceptible to experience huge change at any given moment, with the probability of profit comes huge risks of losing a fortune. In our research, we have worked on prediction of breakout stocks. If identified properly, it can help one to invest efficiently. We have used multiple machine learning approaches as ML models can offer more effective predictions compared to other methods due to the ability to learn and adapt from dataset information. In our experiment, the models have yielded very good results." - }, - { - "id": "3", - "title": "Weak Form of Market Efficiency Trading Range Breakout Test on Weekly Stocks of India Markets", - "authors": "No authors available", - "published": 2017, - "URL": "http://dx.doi.org/10.22259/ijrbsm.0402002", - "DOI": "10.22259/ijrbsm.0402002", - "abstract": "No abstract available." - }, - { - "id": "4", - "title": "Challenges and Solutions for Automated Wellbore Status Monitoring - Breakout Detection as an Example", - "authors": "Stefan Wessling, Thomas Dahl, Dinah Pantic", - "published": 2011, - "URL": "http://dx.doi.org/10.2118/143647-ms", - "DOI": "10.2118/143647-ms", - "abstract": "Abstract\n Automated real-time wellbore stability services contribute to a significant reduction of non productive time. Automation reduces the workload, supports the decisions of engineers and facilitates simultaneous remote supervision of multiple wells. The industry is approaching first steps towards automated drilling; however the accomplishment of mature automated systems requires further extensive research and development. In particular, automated real-time wellbore stability systems require a high level of confidence because decisions based on the results can have significant consequences. Robust, reliable and intensively tested algorithms need to be developed and experience has to be gathered by comprehensively supervised field testing.\n This paper presents an algorithm and its applications for the automated monitoring of wellbore status. Taking the detection of breakouts on images of the borehole wall as an example, observations and experience gained during the development of such a system are documented. A breakout detection system is fed by formation evaluation and drilling dynamics logs acquired by downhole tools during the drilling operation. The automated breakout detection algorithm is able to scan image log data to identify the existence or non-existence of breakouts and to deliver parameters such as the breakout orientation and width for the calibration of in-situ Earth stress directions and magnitudes needed for calculation of the shear failure gradient (one of two lower bounds for the pressure window).\n The automatic detection of breakouts on low-resolution resistivity and density real-time images proves the algorithm's applicability while drilling. The tests were also used to identify operating constraints, i.e., circumstances (especially of geological nature) under which the system may deliver erroneous information. Users need to be aware of such operating constrains to have confidence in the algorithm's reliability." - }, - { - "id": "5", - "title": "Breakout Groups Assignments and Instructions", - "authors": "No authors available", - "published": 2008, - "URL": "http://dx.doi.org/10.4016/6678.01", - "DOI": "10.4016/6678.01", - "abstract": "No abstract available." - } -] \ No newline at end of file diff --git a/output.html b/output.html deleted file mode 100644 index 8c083235..00000000 --- a/output.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - QuantCLI Search Results - - - -

Search Results

- - - - \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e0ab07d9..64e64c1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,13 @@ dependencies = [ [project.optional-dependencies] dev = [ "pytest>=7.4.0", + "pytest-cov>=4.0", + "pytest-mock>=3.10", "black>=23.0.0", "ruff>=0.1.0", "mypy>=1.7.0", + "pre-commit>=3.0", + "pip-audit>=2.6", ] [project.scripts] @@ -69,7 +73,63 @@ target-version = ['py310'] line-length = 100 target-version = "py310" +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "S", # flake8-bandit (security) +] +ignore = [ + "E501", # line too long (handled by black) + "S101", # use of assert (ok in tests) +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["S101"] + [tool.mypy] python_version = "3.10" warn_return_any = true warn_unused_configs = true +ignore_missing_imports = true +show_error_codes = true + +[[tool.mypy.overrides]] +module = [ + "pdfplumber.*", + "spacy.*", + "pygments.*", + "InquirerPy.*", + "rich.*", + "toml.*", +] +ignore_missing_imports = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_functions = ["test_*"] +addopts = ["-v", "--tb=short"] +markers = [ + "slow: marks tests as slow", + "integration: marks tests as integration tests", +] + +[tool.coverage.run] +source = ["quantcoder"] +branch = true +omit = ["*/tests/*"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", +] diff --git a/quantcli.log b/quantcli.log deleted file mode 100644 index 3827ec07..00000000 --- a/quantcli.log +++ /dev/null @@ -1,94 +0,0 @@ -2025-03-23 19:30:42,846 - quantcli.utils - INFO - Logging is set up. -2025-03-23 19:30:42,846 - quantcli.utils - ERROR - OPENAI_API_KEY not found in the environment variables. -2025-03-23 19:37:20,792 - quantcli.utils - INFO - Logging is set up. -2025-03-23 19:37:20,792 - quantcli.utils - INFO - OpenAI API key loaded and set globally. -2025-03-23 19:37:25,417 - HeadingDetector - ERROR - Failed to load SpaCy model 'en_core_web_sm': [E050] Can't find model 'en_core_web_sm'. It doesn't seem to be a Python package or a valid path to a data directory. -2025-03-23 19:44:27,120 - quantcli.utils - INFO - Logging is set up. -2025-03-23 19:44:27,122 - quantcli.utils - INFO - OpenAI API key loaded and set globally. -2025-03-23 19:44:33,216 - HeadingDetector - INFO - SpaCy model 'en_core_web_sm' loaded successfully. -2025-03-23 19:44:33,216 - OpenAIHandler - INFO - Generating QuantConnect code using OpenAI. -2025-03-23 19:44:33,216 - OpenAIHandler - ERROR - OpenAI API error during code generation: - -You tried to access openai.ChatCompletion, but this is no longer supported in openai>=1.0.0 - see the README at https://github.com/openai/openai-python for the API. - -You can run `openai migrate` to automatically upgrade your codebase to use the 1.0.0 interface. - -Alternatively, you can pin your installation to the old version, e.g. `pip install openai==0.28` - -A detailed migration guide is available here: https://github.com/openai/openai-python/discussions/742 - -2025-03-23 19:44:56,518 - HeadingDetector - INFO - SpaCy model 'en_core_web_sm' loaded successfully. -2025-03-23 19:44:56,528 - ArticleProcessor - INFO - Starting extraction process for PDF: C:/Users/slrig/Downloads/Trading range breakout.pdf -2025-03-23 19:44:56,534 - PDFLoader - INFO - Loading PDF: C:/Users/slrig/Downloads/Trading range breakout.pdf -2025-03-23 19:44:57,644 - PDFLoader - INFO - PDF loaded successfully. -2025-03-23 19:44:57,644 - TextPreprocessor - INFO - Starting text preprocessing. -2025-03-23 19:44:57,644 - TextPreprocessor - INFO - Text preprocessed successfully. Reduced from 18434 to 17787 characters. -2025-03-23 19:44:57,657 - HeadingDetector - INFO - Starting heading detection. -2025-03-23 19:44:58,229 - HeadingDetector - INFO - Detected 0 headings. -2025-03-23 19:44:58,229 - ArticleProcessor - WARNING - No headings detected. Proceeding with default sectioning. -2025-03-23 19:44:58,229 - SectionSplitter - INFO - Starting section splitting. -2025-03-23 19:44:58,242 - SectionSplitter - INFO - Split text into 1 sections. -2025-03-23 19:44:58,242 - KeywordAnalyzer - INFO - Starting keyword analysis. -2025-03-23 19:44:58,248 - KeywordAnalyzer - INFO - Keyword analysis completed. -2025-03-23 19:44:58,248 - OpenAIHandler - INFO - Generating summary using OpenAI. -2025-03-23 19:44:58,248 - OpenAIHandler - ERROR - OpenAI API error during summary generation: - -You tried to access openai.ChatCompletion, but this is no longer supported in openai>=1.0.0 - see the README at https://github.com/openai/openai-python for the API. - -You can run `openai migrate` to automatically upgrade your codebase to use the 1.0.0 interface. - -Alternatively, you can pin your installation to the old version, e.g. `pip install openai==0.28` - -A detailed migration guide is available here: https://github.com/openai/openai-python/discussions/742 - -2025-03-23 19:46:52,843 - quantcli.utils - INFO - Logging is set up. -2025-03-23 19:46:52,843 - quantcli.utils - INFO - OpenAI API key loaded and set globally. -2025-03-23 19:46:59,493 - HeadingDetector - INFO - SpaCy model 'en_core_web_sm' loaded successfully. -2025-03-23 19:46:59,495 - OpenAIHandler - INFO - Generating QuantConnect code using OpenAI. -2025-03-23 19:47:11,820 - OpenAIHandler - INFO - QuantConnect code generated successfully. -2025-03-23 19:47:11,820 - CodeValidator - INFO - Validating generated code for syntax errors. -2025-03-23 19:47:11,820 - CodeValidator - ERROR - Syntax error in generated code: invalid syntax (, line 1) -2025-03-23 19:47:11,820 - CodeRefiner - INFO - Refining code using OpenAI. -2025-03-23 19:47:11,834 - OpenAIHandler - INFO - Refining generated code using OpenAI. -2025-03-23 19:47:25,534 - OpenAIHandler - INFO - Code refined successfully. -2025-03-23 19:47:38,478 - HeadingDetector - INFO - SpaCy model 'en_core_web_sm' loaded successfully. -2025-03-23 19:47:38,478 - ArticleProcessor - INFO - Starting extraction process for PDF: C:/Users/slrig/Downloads/Trading range breakout.pdf -2025-03-23 19:47:38,480 - PDFLoader - INFO - Loading PDF: C:/Users/slrig/Downloads/Trading range breakout.pdf -2025-03-23 19:47:39,552 - PDFLoader - INFO - PDF loaded successfully. -2025-03-23 19:47:39,552 - TextPreprocessor - INFO - Starting text preprocessing. -2025-03-23 19:47:39,552 - TextPreprocessor - INFO - Text preprocessed successfully. Reduced from 18434 to 17787 characters. -2025-03-23 19:47:39,552 - HeadingDetector - INFO - Starting heading detection. -2025-03-23 19:47:40,170 - HeadingDetector - INFO - Detected 0 headings. -2025-03-23 19:47:40,171 - ArticleProcessor - WARNING - No headings detected. Proceeding with default sectioning. -2025-03-23 19:47:40,171 - SectionSplitter - INFO - Starting section splitting. -2025-03-23 19:47:40,171 - SectionSplitter - INFO - Split text into 1 sections. -2025-03-23 19:47:40,171 - KeywordAnalyzer - INFO - Starting keyword analysis. -2025-03-23 19:47:40,175 - KeywordAnalyzer - INFO - Keyword analysis completed. -2025-03-23 19:47:40,175 - OpenAIHandler - INFO - Generating summary using OpenAI. -2025-03-23 19:47:44,489 - OpenAIHandler - INFO - Summary generated successfully. -2025-03-23 19:47:55,540 - HeadingDetector - INFO - SpaCy model 'en_core_web_sm' loaded successfully. -2025-03-23 19:47:55,540 - OpenAIHandler - INFO - Generating QuantConnect code using OpenAI. -2025-03-23 19:48:03,574 - OpenAIHandler - INFO - QuantConnect code generated successfully. -2025-03-23 19:48:03,576 - CodeValidator - INFO - Validating generated code for syntax errors. -2025-03-23 19:48:03,578 - CodeValidator - ERROR - Syntax error in generated code: invalid syntax (, line 1) -2025-03-23 19:48:03,578 - CodeRefiner - INFO - Refining code using OpenAI. -2025-03-23 19:48:03,578 - OpenAIHandler - INFO - Refining generated code using OpenAI. -2025-03-23 19:48:20,802 - OpenAIHandler - INFO - Code refined successfully. -2025-03-23 19:48:20,805 - CodeValidator - INFO - Validating generated code for syntax errors. -2025-03-23 19:48:20,811 - CodeValidator - INFO - Generated code is syntactically correct. -2025-03-23 19:48:20,812 - CodeValidator - INFO - Validating generated code for syntax errors. -2025-03-23 19:48:20,817 - CodeValidator - INFO - Generated code is syntactically correct. -2025-03-23 20:05:23,544 - quantcli.utils - INFO - Logging is set up. -2025-03-23 20:05:23,544 - quantcli.utils - INFO - OpenAI API key loaded and set globally. -2025-03-23 20:05:29,534 - HeadingDetector - INFO - SpaCy model 'en_core_web_sm' loaded successfully. -2025-03-23 20:05:29,534 - OpenAIHandler - INFO - Generating QuantConnect code using OpenAI. -2025-03-23 20:05:43,191 - OpenAIHandler - INFO - QuantConnect code generated successfully. -2025-03-23 20:05:43,191 - CodeValidator - INFO - Validating generated code for syntax errors. -2025-03-23 20:05:43,194 - CodeValidator - ERROR - Syntax error in generated code: invalid syntax (, line 1) -2025-03-23 20:05:43,194 - CodeRefiner - INFO - Refining code using OpenAI. -2025-03-23 20:05:43,200 - OpenAIHandler - INFO - Refining generated code using OpenAI. -2025-03-23 20:05:57,315 - OpenAIHandler - INFO - Code refined successfully. -2025-03-23 20:05:57,316 - CodeValidator - INFO - Validating generated code for syntax errors. -2025-03-23 20:05:57,322 - CodeValidator - INFO - Generated code is syntactically correct. -2025-03-23 20:05:57,323 - CodeValidator - INFO - Validating generated code for syntax errors. -2025-03-23 20:05:57,329 - CodeValidator - INFO - Generated code is syntactically correct. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..ccc0423a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package for quantcoder diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..69cfba9c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,110 @@ +"""Pytest fixtures and configuration for quantcoder tests.""" + +import pytest +from unittest.mock import MagicMock + + +@pytest.fixture +def mock_openai_client(): + """Create a mock OpenAI client for testing.""" + client = MagicMock() + + # Mock chat completions response + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "Test response" + + client.chat.completions.create.return_value = mock_response + + return client + + +@pytest.fixture +def sample_extracted_data(): + """Sample extracted data for testing.""" + return { + "trading_signal": [ + "Buy when RSI crosses above 30", + "Sell when RSI crosses below 70", + ], + "risk_management": [ + "Use 2% position sizing", + "Set stop loss at 10% below entry", + ], + } + + +@pytest.fixture +def sample_pdf_text(): + """Sample text that would be extracted from a PDF.""" + return """ + Trading Strategy Overview + + This strategy uses a momentum-based approach with RSI indicators. + Buy signals are generated when RSI crosses above 30 from oversold territory. + Sell signals occur when RSI drops below 70 from overbought levels. + + Risk Management + + Position sizing is limited to 2% of portfolio per trade. + Stop loss is set at 10% below entry price to limit downside risk. + Maximum drawdown tolerance is 20%. + """ + + +@pytest.fixture +def sample_python_code(): + """Sample valid Python code for testing.""" + return ''' +from AlgorithmImports import * + +class MomentumStrategy(QCAlgorithm): + def Initialize(self): + self.SetStartDate(2020, 1, 1) + self.SetEndDate(2023, 12, 31) + self.SetCash(100000) + self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol + self.rsi = self.RSI(self.symbol, 14) + + def OnData(self, data): + if not self.rsi.IsReady: + return + if self.rsi.Current.Value < 30: + self.SetHoldings(self.symbol, 1.0) + elif self.rsi.Current.Value > 70: + self.Liquidate(self.symbol) +''' + + +@pytest.fixture +def invalid_python_code(): + """Sample invalid Python code for testing.""" + return """ +def broken_function( + # Missing closing parenthesis and body +""" + + +@pytest.fixture +def mock_config(): + """Mock configuration object for testing.""" + config = MagicMock() + config.api_key = "sk-test-key-12345" + config.load_api_key.return_value = "sk-test-key-12345" + config.model.model = "gpt-4o" + config.model.temperature = 0.5 + config.model.max_tokens = 1000 + return config + + +@pytest.fixture +def env_with_api_key(monkeypatch): + """Set up environment with mock API key.""" + monkeypatch.setenv("OPENAI_API_KEY", "sk-test-key-12345") + return "sk-test-key-12345" + + +@pytest.fixture +def env_without_api_key(monkeypatch): + """Set up environment without API key.""" + monkeypatch.delenv("OPENAI_API_KEY", raising=False) diff --git a/tests/test_llm.py b/tests/test_llm.py new file mode 100644 index 00000000..6e513b70 --- /dev/null +++ b/tests/test_llm.py @@ -0,0 +1,66 @@ +"""Tests for the quantcoder.core.llm module.""" + +import pytest +from unittest.mock import MagicMock, patch + +from quantcoder.core.llm import LLMHandler + + +class TestLLMHandler: + """Tests for LLMHandler class.""" + + def test_init_with_config(self, mock_config, mock_openai_client): + """Test initialization with config.""" + with patch("quantcoder.core.llm.OpenAI", return_value=mock_openai_client): + handler = LLMHandler(mock_config) + assert handler.model == "gpt-4o" + assert handler.temperature == 0.5 + + def test_generate_summary(self, mock_config, mock_openai_client, sample_extracted_data): + """Test summary generation.""" + with patch("quantcoder.core.llm.OpenAI", return_value=mock_openai_client): + handler = LLMHandler(mock_config) + result = handler.generate_summary(sample_extracted_data) + + assert result is not None + mock_openai_client.chat.completions.create.assert_called_once() + + def test_generate_qc_code(self, mock_config, mock_openai_client): + """Test QuantConnect code generation.""" + with patch("quantcoder.core.llm.OpenAI", return_value=mock_openai_client): + handler = LLMHandler(mock_config) + result = handler.generate_qc_code("Test strategy summary") + + assert result is not None + mock_openai_client.chat.completions.create.assert_called_once() + + def test_extract_code_from_markdown(self, mock_config, mock_openai_client): + """Test extraction of code from markdown blocks.""" + with patch("quantcoder.core.llm.OpenAI", return_value=mock_openai_client): + handler = LLMHandler(mock_config) + + markdown_response = """```python +def test(): + pass +```""" + result = handler._extract_code_from_response(markdown_response) + assert result == "def test():\n pass" + + def test_extract_code_without_markdown(self, mock_config, mock_openai_client): + """Test extraction when response has no markdown.""" + with patch("quantcoder.core.llm.OpenAI", return_value=mock_openai_client): + handler = LLMHandler(mock_config) + + plain_response = "def test():\n pass" + result = handler._extract_code_from_response(plain_response) + assert result == plain_response + + def test_handles_api_error(self, mock_config, mock_openai_client, sample_extracted_data): + """Test handling of API errors.""" + mock_openai_client.chat.completions.create.side_effect = Exception("API Error") + + with patch("quantcoder.core.llm.OpenAI", return_value=mock_openai_client): + handler = LLMHandler(mock_config) + result = handler.generate_summary(sample_extracted_data) + + assert result is None diff --git a/tests/test_processor.py b/tests/test_processor.py new file mode 100644 index 00000000..7f1cae53 --- /dev/null +++ b/tests/test_processor.py @@ -0,0 +1,125 @@ +"""Tests for the quantcoder.core.processor module.""" + +import pytest +from unittest.mock import MagicMock, patch + +from quantcoder.core.processor import ( + TextPreprocessor, + CodeValidator, + KeywordAnalyzer, + SectionSplitter, +) + + +class TestTextPreprocessor: + """Tests for TextPreprocessor class.""" + + def test_preprocess_removes_urls(self): + """Test that URLs are removed from text.""" + preprocessor = TextPreprocessor() + text = "Visit https://example.com for more info" + result = preprocessor.preprocess_text(text) + assert "https://example.com" not in result + + def test_preprocess_removes_electronic_copy_phrase(self): + """Test that 'Electronic copy available at' phrases are removed.""" + preprocessor = TextPreprocessor() + text = "Some text Electronic copy available at: ssrn.com more text" + result = preprocessor.preprocess_text(text) + assert "Electronic copy available at" not in result + + def test_preprocess_collapses_multiple_newlines(self): + """Test that multiple newlines are collapsed to single newlines.""" + preprocessor = TextPreprocessor() + text = "Line 1\n\n\n\nLine 2" + result = preprocessor.preprocess_text(text) + assert "\n\n" not in result + + def test_preprocess_strips_whitespace(self): + """Test that leading/trailing whitespace is stripped.""" + preprocessor = TextPreprocessor() + text = " Some text " + result = preprocessor.preprocess_text(text) + assert result == "Some text" + + +class TestSectionSplitter: + """Tests for SectionSplitter class.""" + + def test_split_with_headings(self): + """Test splitting text with detected headings.""" + splitter = SectionSplitter() + text = "Introduction\nThis is intro.\nMethods\nThis is methods." + headings = ["Introduction", "Methods"] + sections = splitter.split_into_sections(text, headings) + + assert "Introduction" in sections + assert "Methods" in sections + assert "intro" in sections["Introduction"].lower() + assert "methods" in sections["Methods"].lower() + + def test_split_without_headings(self): + """Test splitting text when no headings are detected.""" + splitter = SectionSplitter() + text = "This is all introduction text." + headings = [] + sections = splitter.split_into_sections(text, headings) + + assert "Introduction" in sections # Default section + + +class TestKeywordAnalyzer: + """Tests for KeywordAnalyzer class.""" + + def test_detects_trading_signals(self): + """Test detection of trading signal keywords.""" + analyzer = KeywordAnalyzer() + sections = {"Strategy": "Buy when the trend is up. Sell when RSI is high."} + result = analyzer.keyword_analysis(sections) + + assert "trading_signal" in result + assert len(result["trading_signal"]) > 0 + + def test_detects_risk_management(self): + """Test detection of risk management keywords.""" + analyzer = KeywordAnalyzer() + sections = {"Risk": "Limit drawdown to 10%. Reduce volatility exposure."} + result = analyzer.keyword_analysis(sections) + + assert "risk_management" in result + assert len(result["risk_management"]) > 0 + + def test_skips_irrelevant_patterns(self): + """Test that irrelevant patterns are skipped.""" + analyzer = KeywordAnalyzer() + sections = {"Figures": "See figure 1 for buy signal details."} + result = analyzer.keyword_analysis(sections) + + # Should not include sentences with "figure X" pattern + for sentence in result.get("trading_signal", []): + assert "figure 1" not in sentence.lower() + + +class TestCodeValidator: + """Tests for CodeValidator class.""" + + def test_validates_correct_code(self, sample_python_code): + """Test validation of syntactically correct code.""" + validator = CodeValidator() + assert validator.validate_code(sample_python_code) is True + + def test_rejects_invalid_code(self, invalid_python_code): + """Test rejection of syntactically invalid code.""" + validator = CodeValidator() + assert validator.validate_code(invalid_python_code) is False + + def test_handles_empty_code(self): + """Test handling of empty code string.""" + validator = CodeValidator() + # Empty string is valid Python + assert validator.validate_code("") is True + + def test_handles_simple_expression(self): + """Test validation of simple expressions.""" + validator = CodeValidator() + assert validator.validate_code("x = 1 + 2") is True