"""Tests for analytics/extract.py — runs without MT5 or Wine."""
import json
import os
import sys
import tempfile
from pathlib import Path
import pytest
FIXTURES = Path(__file__).parent / 'fixtures'
sys.path.insert(0, str(Path(__file__).parent.parent))
from analytics.extract import (
detect_format, parse_html, parse_xml, write_outputs,
_parse_metrics_html, _parse_deals_html,
)
def test_detect_format_html():
assert detect_format(str(FIXTURES / 'sample_report.htm')) == 'html'
def test_detect_format_xml():
assert detect_format(str(FIXTURES / 'sample_report.htm.xml')) == 'xml'
# HTML parsing is tested via internal functions to avoid the UTF-16 decode dance
# (read_text tries UTF-16 first, which silently garbles plain UTF-8/ASCII files).
# The fixture is used for format-detection only.
HTML_TEXT = """
| Net profit | 1234.56 |
| Profit factor | 1.25 |
| Maximal drawdown | 500.00 (5.00%) |
| Sharpe Ratio | 0.75 |
| Total trades | 150 |
| Recovery factor | 2.50 |
| Profit trades (% of total) | 90 (60.00%) |
| Gross profit | 2000.00 |
| Gross loss | -765.44 |
| Deal Time | Type | Direction | Volume | Price | S/L | T/P | Profit | Balance | Comment | Order | Magic | Entry |
| 2025.01.10 09:30:00 | buy | out | 0.01 | 1915.00 | 0 | 0 | 15.00 | 10015.00 | Layer #1 | 1001 | 12345 | out |
| 2025.02.05 14:00:00 | sell | out | 0.01 | 1945.00 | 0 | 0 | -15.00 | 10020.00 | Layer #1 | 1005 | 12345 | out |
"""
@pytest.fixture
def html_report_path(tmp_path):
"""Write HTML fixture as UTF-16 LE with BOM so read_text() decodes it correctly."""
p = tmp_path / 'report.htm'
p.write_bytes(b'\xff\xfe' + HTML_TEXT.encode('utf-16-le'))
return str(p)
def test_parse_html_returns_metrics(html_report_path):
metrics, _ = parse_html(html_report_path)
assert isinstance(metrics, dict)
assert 'net_profit' in metrics
assert metrics['net_profit'] == pytest.approx(1234.56)
assert metrics['total_trades'] == 150
def test_parse_html_returns_deals(html_report_path):
_, deals = parse_html(html_report_path)
assert isinstance(deals, list)
assert len(deals) >= 1
deal = deals[0]
assert 'profit' in deal
assert 'balance' in deal
def test_parse_metrics_html_directly():
"""Test HTML metric extraction without encoding layer."""
metrics = _parse_metrics_html(HTML_TEXT)
assert metrics['net_profit'] == pytest.approx(1234.56)
assert metrics['profit_factor'] == pytest.approx(1.25)
assert metrics['max_dd_pct'] == pytest.approx(5.00)
assert metrics['total_trades'] == 150
def test_parse_deals_html_directly():
"""Test HTML deal extraction without encoding layer."""
deals = _parse_deals_html(HTML_TEXT)
assert len(deals) == 2
assert float(deals[0]['profit']) == pytest.approx(15.00)
assert float(deals[1]['profit']) == pytest.approx(-15.00)
def test_parse_xml_returns_metrics():
metrics, deals = parse_xml(str(FIXTURES / 'sample_report.htm.xml'))
assert isinstance(metrics, dict)
assert 'net_profit' in metrics
assert metrics['net_profit'] == pytest.approx(1234.56)
assert metrics['total_trades'] == 150
def test_parse_xml_returns_deals():
metrics, deals = parse_xml(str(FIXTURES / 'sample_report.htm.xml'))
assert isinstance(deals, list)
assert len(deals) >= 1
deal = deals[0]
assert deal.get('profit') is not None
def test_write_outputs_creates_files():
metrics = {'net_profit': 100.0, 'total_trades': 5}
deals = [
{'time': '2025.01.10', 'type': 'buy', 'direction': 'out', 'volume': '0.01',
'price': '1900', 'sl': '0', 'tp': '0', 'profit': '10.00',
'balance': '10010', 'comment': '', 'order': '1', 'magic': '1', 'entry': 'out'},
]
with tempfile.TemporaryDirectory() as tmp:
paths = write_outputs(metrics, deals, tmp)
assert Path(paths['metrics']).exists()
assert Path(paths['deals_csv']).exists()
assert Path(paths['deals_json']).exists()
# Verify metrics.json content
with open(paths['metrics']) as f:
saved = json.load(f)
assert saved['net_profit'] == 100.0
def test_parse_html_skips_balance_rows():
"""Rows with type='balance' should be filtered out."""
html = """
| Deal Time | Type | Direction | Volume | Price | S/L | T/P | Profit | Balance | Comment | Order | Magic | Entry |
| 2025.01.10 09:30:00 | balance | | 0 | 0 | 0 | 0 | 0 | 10000 | | 0 | 0 | |
| 2025.01.10 10:00:00 | buy | out | 0.01 | 1910 | 0 | 0 | 5.00 | 10005 | Layer #1 | 1 | 1 | out |
"""
import tempfile, os
with tempfile.NamedTemporaryFile(mode='w', suffix='.htm', delete=False) as f:
f.write(html)
path = f.name
try:
_, deals = parse_html(path)
types = [d.get('type', '').lower() for d in deals]
assert 'balance' not in types
finally:
os.unlink(path)