Files

725 lines
27 KiB
Python

"""Comprehensive tests for the Result module.
Tests cover:
- Result initialization with various parameters
- Auto-time behavior in __init__
- get_data method for preparing trade data
- save async method with different trade record modes
- save_sync method with different trade record modes
- to_csv method for CSV file storage
- to_json method for JSON file storage
- to_sql method for SQLite database storage
- serialize static method
- Thread safety with Lock
- Edge cases and error handling
- OrderSendResult None field filtering
"""
import asyncio
import csv
import json
from pathlib import Path
from datetime import datetime
from threading import Thread
import pytest
from aiomql.lib.result import Result
from aiomql.core.models import OrderSendResult, TradeRequest
from aiomql.core.config import Config
class TestResultInitialization:
"""Test Result class initialization."""
@pytest.fixture(scope="class")
def mock_order_result(self):
"""Create a mock OrderSendResult with TradeRequest."""
request = TradeRequest(
action=1,
type=0,
order=0,
symbol="EURUSD",
volume=0.01,
sl=1.0800,
tp=1.0900,
price=1.0850,
deviation=10,
stop_limit=0,
type_time=0,
type_filling=0,
expiration=0,
position=0,
position_by=0,
comment="",
magic=0
)
osr = OrderSendResult(
retcode=10009,
deal=12345,
order=67890,
volume=0.01,
price=1.0850,
bid=1.0849,
ask=1.0851,
comment="",
request_id=0,
retcode_external=0,
)
osr.request = request
return osr
def test_init_with_result_only(self, mock_order_result):
"""Test Result can be initialized with just a result."""
result = Result(result=mock_order_result)
assert result.result == mock_order_result
assert result.parameters == {}
assert result.name == "Trades"
def test_init_with_parameters(self, mock_order_result):
"""Test Result initialized with parameters."""
params = {"symbol": "EURUSD", "strategy": "MA_Cross"}
result = Result(result=mock_order_result, parameters=params)
assert result.parameters == params
assert result.result == mock_order_result
def test_init_with_name(self, mock_order_result):
"""Test Result initialized with explicit name."""
result = Result(result=mock_order_result, name="MyStrategy")
assert result.name == "MyStrategy"
def test_init_name_from_parameters(self, mock_order_result):
"""Test Result name defaults to 'name' key in parameters."""
params = {"name": "ParameterName", "symbol": "EURUSD"}
result = Result(result=mock_order_result, parameters=params)
assert result.name == "ParameterName"
def test_init_explicit_name_overrides_parameters(self, mock_order_result):
"""Test explicit name overrides parameters name."""
params = {"name": "ParameterName", "symbol": "EURUSD"}
result = Result(result=mock_order_result, parameters=params, name="ExplicitName")
assert result.name == "ExplicitName"
def test_init_with_extra_params(self, mock_order_result):
"""Test Result initialized with extra keyword arguments."""
result = Result(
result=mock_order_result,
time=1705312800000,
expected_profit=10.5
)
assert result.extra_params["time"] == 1705312800000
assert result.extra_params["expected_profit"] == 10.5
def test_init_has_config(self, mock_order_result):
"""Test Result has Config instance."""
result = Result(result=mock_order_result)
assert hasattr(result, 'config')
assert isinstance(result.config, Config)
def test_init_auto_sets_time_in_extra_params(self, mock_order_result):
"""Test Result auto-sets time in extra_params when not provided."""
result = Result(result=mock_order_result)
assert "time" in result.extra_params
assert isinstance(result.extra_params["time"], float)
assert result.extra_params["time"] > 0
def test_init_preserves_explicit_time_in_extra_params(self, mock_order_result):
"""Test Result preserves explicitly provided time."""
explicit_time = 1705312800000
result = Result(result=mock_order_result, time=explicit_time)
assert result.extra_params["time"] == explicit_time
def test_init_has_lock_class_attribute(self, mock_order_result):
"""Test Result class has Lock attribute."""
assert hasattr(Result, 'lock')
class TestGetData:
"""Test get_data method."""
@pytest.fixture(scope="class")
def mock_order_result(self):
"""Create a mock OrderSendResult with TradeRequest."""
request = TradeRequest(
action=1,
type=0,
order=0,
symbol="EURUSD",
volume=0.01,
sl=1.0800,
tp=1.0900,
price=1.0850,
deviation=10,
stop_limit=0,
type_time=0,
type_filling=0,
expiration=0,
position=0,
position_by=0,
comment="Test comment",
magic=0
)
osr = OrderSendResult(
retcode=10009,
deal=12345,
order=67890,
volume=0.01,
price=1.0850,
bid=1.0849,
ask=1.0851,
comment="Test comment",
request_id=100,
retcode_external=0,
)
osr.request = request
return osr
def test_get_data_returns_dict(self, mock_order_result):
"""Test get_data returns a dictionary."""
result = Result(result=mock_order_result)
data = result.get_data()
assert isinstance(data, dict)
def test_get_data_includes_order_fields(self, mock_order_result):
"""Test get_data includes order result fields."""
result = Result(result=mock_order_result)
data = result.get_data()
assert "deal" in data
assert "order" in data
assert "volume" in data
assert "price" in data
def test_get_data_excludes_retcode_fields(self, mock_order_result):
"""Test get_data excludes retcode and comment fields."""
result = Result(result=mock_order_result)
data = result.get_data()
assert "retcode" not in data
assert "comment" not in data
assert "retcode_external" not in data
assert "request_id" not in data
def test_get_data_excludes_request_object(self, mock_order_result):
"""Test get_data excludes the request object."""
result = Result(result=mock_order_result)
data = result.get_data()
assert "request" not in data
def test_get_data_includes_request_fields(self, mock_order_result):
"""Test get_data includes symbol, type, sl, tp from request."""
result = Result(result=mock_order_result)
data = result.get_data()
assert "symbol" in data
assert "type" in data
assert "sl" in data
assert "tp" in data
def test_get_data_includes_default_tracking_fields(self, mock_order_result):
"""Test get_data includes default tracking fields."""
result = Result(result=mock_order_result)
data = result.get_data()
assert data["profit"] == 0
assert data["closed"] == False
assert data["win"] == False
def test_get_data_includes_parameters_key(self, mock_order_result):
"""Test get_data includes parameters dict under 'parameters' key."""
params = {"ema": 20, "rsi": 14}
result = Result(result=mock_order_result, parameters=params)
data = result.get_data()
assert "parameters" in data
assert data["parameters"] == params
def test_get_data_includes_parameters(self, mock_order_result):
"""Test get_data includes strategy parameters."""
params = {"symbol": "EURUSD", "ema": 20, "rsi": 14}
result = Result(result=mock_order_result, parameters=params)
data = result.get_data()
assert data["symbol"] == "EURUSD"
assert data["parameters"]["ema"] == 20
assert data["parameters"]["rsi"] == 14
def test_get_data_includes_time_key(self, mock_order_result):
"""Test get_data includes auto-set time from extra_params."""
result = Result(result=mock_order_result)
data = result.get_data()
assert "time" in data
assert isinstance(data["time"], float)
def test_get_data_includes_extra_params(self, mock_order_result):
"""Test get_data includes extra parameters."""
result = Result(
result=mock_order_result,
time=1705312800000,
expected_profit=15.0
)
data = result.get_data()
assert data["time"] == 1705312800000
assert data["expected_profit"] == 15.0
class TestSerialize:
"""Test serialize static method."""
def test_serialize_string(self):
"""Test serialize converts string."""
assert Result.serialize("hello") == "hello"
def test_serialize_integer(self):
"""Test serialize converts integer."""
assert Result.serialize(42) == "42"
def test_serialize_float(self):
"""Test serialize converts float."""
assert Result.serialize(3.14) == "3.14"
def test_serialize_list(self):
"""Test serialize converts list."""
result = Result.serialize([1, 2, 3])
assert result == "[1, 2, 3]"
def test_serialize_dict(self):
"""Test serialize converts dict."""
result = Result.serialize({"key": "value"})
assert "key" in result
assert "value" in result
def test_serialize_datetime(self):
"""Test serialize converts datetime."""
dt = datetime(2024, 1, 15, 10, 30, 0)
result = Result.serialize(dt)
assert "2024" in result
assert "01" in result
assert "15" in result
def test_serialize_none(self):
"""Test serialize converts None."""
assert Result.serialize(None) == "None"
def test_serialize_boolean(self):
"""Test serialize converts boolean."""
assert Result.serialize(True) == "True"
assert Result.serialize(False) == "False"
class TestSaveAsync:
"""Test async save method with live trading."""
@pytest.fixture(scope="class")
def parameters(self):
"""Create test parameters."""
return {"name": "test_result_async", "symbol": "BTCUSD", "ema": 20, "rsi": 14}
@pytest.fixture(scope="function")
async def order_result(self, mt, buy_order, parameters):
"""Create an order and return Result instance."""
res = await mt.order_send(buy_order)
return Result(result=OrderSendResult(**res._asdict()), parameters=parameters)
async def test_save_csv_creates_file(self, order_result):
"""Test save with csv mode creates file."""
await order_result.save(trade_record_mode="csv")
file = order_result.config.records_dir / f"{order_result.name}.csv"
assert file.exists()
async def test_save_json_creates_file(self, order_result):
"""Test save with json mode creates file."""
await order_result.save(trade_record_mode="json")
file = order_result.config.records_dir / f"{order_result.name}.json"
assert file.exists()
async def test_save_sql_no_exception(self, order_result):
"""Test save with sql mode does not raise exception."""
await order_result.save(trade_record_mode="sql")
async def test_save_invalid_mode_no_exception(self, order_result):
"""Test save with invalid mode does not raise but logs error."""
await order_result.save(trade_record_mode="invalid_mode")
async def test_save_uses_default_mode(self, order_result):
"""Test save uses config default mode when not specified."""
original_mode = order_result.config.trade_record_mode
order_result.config.trade_record_mode = "csv"
await order_result.save()
file = order_result.config.records_dir / f"{order_result.name}.csv"
assert file.exists()
order_result.config.trade_record_mode = original_mode
class TestSaveSync:
"""Test sync save method with live trading."""
@pytest.fixture(scope="class")
def parameters(self):
"""Create test parameters."""
return {"name": "test_result_sync", "symbol": "BTCUSD", "ema": 20}
@pytest.fixture(scope="function")
async def order_result(self, mt, sell_order, parameters):
"""Create an order and return Result instance."""
res = await mt.order_send(sell_order)
return Result(result=OrderSendResult(**res._asdict()), parameters=parameters)
async def test_save_sync_csv(self, order_result):
"""Test save_sync with csv mode."""
order_result.save_sync(trade_record_mode="csv")
file = order_result.config.records_dir / f"{order_result.name}.csv"
assert file.exists()
async def test_save_sync_json(self, order_result):
"""Test save_sync with json mode."""
order_result.save_sync(trade_record_mode="json")
file = order_result.config.records_dir / f"{order_result.name}.json"
assert file.exists()
async def test_save_sync_sql_no_exception(self, order_result):
"""Test save_sync with sql mode does not raise exception."""
order_result.save_sync(trade_record_mode="sql")
class TestToCsv:
"""Test to_csv method."""
@pytest.fixture(scope="class")
def parameters(self):
"""Create test parameters."""
return {"name": "test_csv_method", "symbol": "EURUSD", "strategy": "test"}
@pytest.fixture(scope="function")
async def order_result(self, mt, buy_order, parameters):
"""Create an order and return Result instance."""
res = await mt.order_send(buy_order)
return Result(result=OrderSendResult(**res._asdict()), parameters=parameters)
async def test_to_csv_creates_file(self, order_result):
"""Test to_csv creates CSV file."""
order_result.to_csv()
file = order_result.config.records_dir / f"{order_result.name}.csv"
assert file.exists()
async def test_to_csv_has_headers(self, order_result):
"""Test CSV file has headers."""
order_result.to_csv()
file = order_result.config.records_dir / f"{order_result.name}.csv"
with file.open("r") as fh:
reader = csv.DictReader(fh)
headers = reader.fieldnames
assert "deal" in headers
assert "order" in headers
async def test_to_csv_appends_rows(self, order_result):
"""Test to_csv appends rows to existing file."""
order_result.to_csv()
order_result.to_csv() # Append second row
file = order_result.config.records_dir / f"{order_result.name}.csv"
with file.open("r") as fh:
reader = list(csv.DictReader(fh))
assert len(reader) >= 2
async def test_to_csv_divides_time_by_1000(self, order_result):
"""Test to_csv divides time by 1000 for seconds conversion."""
raw_time = order_result.get_data()["time"]
order_result.to_csv()
file = order_result.config.records_dir / f"{order_result.name}.csv"
with file.open("r") as fh:
reader = list(csv.DictReader(fh))
last_row = reader[-1]
assert float(last_row["time"]) == pytest.approx(raw_time / 1000, rel=1e-3)
async def test_to_csv_flattens_parameters(self, order_result):
"""Test to_csv flattens parameters dict into row columns."""
order_result.to_csv()
file = order_result.config.records_dir / f"{order_result.name}.csv"
with file.open("r") as fh:
reader = list(csv.DictReader(fh))
last_row = reader[-1]
# 'parameters' key should not be in row (it's popped and flattened)
assert "parameters" not in last_row
# Strategy param keys should be present
assert "strategy" in last_row
class TestToJson:
"""Test to_json method."""
@pytest.fixture(scope="class")
def parameters(self):
"""Create test parameters."""
return {"name": "test_json_method", "symbol": "BTCUSD", "strategy": "json_test"}
@pytest.fixture(scope="function")
async def order_result(self, mt, sell_order, parameters):
"""Create an order and return Result instance."""
res = await mt.order_send(sell_order)
return Result(result=OrderSendResult(**res._asdict()), parameters=parameters)
async def test_to_json_creates_file(self, order_result):
"""Test to_json creates JSON file."""
order_result.to_json()
file = order_result.config.records_dir / f"{order_result.name}.json"
assert file.exists()
async def test_to_json_creates_array(self, order_result):
"""Test JSON file contains array."""
order_result.to_json()
file = order_result.config.records_dir / f"{order_result.name}.json"
with file.open("r") as fh:
data = json.load(fh)
assert isinstance(data, list)
async def test_to_json_appends_records(self, order_result):
"""Test to_json appends records to array."""
order_result.to_json()
order_result.to_json() # Append second record
file = order_result.config.records_dir / f"{order_result.name}.json"
with file.open("r") as fh:
data = json.load(fh)
assert len(data) >= 2
async def test_to_json_record_has_required_fields(self, order_result):
"""Test JSON record has required fields."""
order_result.to_json()
file = order_result.config.records_dir / f"{order_result.name}.json"
with file.open("r") as fh:
data = json.load(fh)
record = data[-1] # Last record
assert "deal" in record
assert "order" in record
assert "profit" in record
assert "closed" in record
assert "win" in record
class TestToSql:
"""Test to_sql method."""
@pytest.fixture(scope="class")
def parameters(self):
"""Create test parameters with required symbol."""
return {"name": "test_sql_method", "symbol": "EURUSD", "strategy": "sql_test"}
@pytest.fixture(scope="function")
async def order_result(self, mt, buy_order, parameters):
"""Create an order and return Result instance."""
res = await mt.order_send(buy_order)
return Result(
result=OrderSendResult(**res._asdict()),
parameters=parameters,
time=int(datetime.now().timestamp() * 1000)
)
async def test_to_sql_no_exception(self, order_result):
"""Test to_sql does not raise exception."""
# Should not raise
order_result.to_sql()
async def test_to_sql_filters_data_to_result_db_fields(self, order_result):
"""Test to_sql uses filter_dict to pass only valid ResultDB fields."""
from aiomql.lib.result_db import ResultDB
data = order_result.get_data() | {"name": order_result.name}
filtered = ResultDB.filter_dict(data)
# Only ResultDB field names should be in filtered data
valid_fields = set(ResultDB.fields())
assert set(filtered.keys()).issubset(valid_fields)
class TestConcurrentSaves:
"""Test concurrent save operations."""
@pytest.fixture(scope="class")
def parameters(self):
"""Create test parameters."""
return {"name": "test_concurrent", "symbol": "BTCUSD"}
@pytest.fixture(scope="function")
async def order_results(self, mt, buy_order, sell_order, parameters):
"""Create multiple orders and return Result instances."""
res1 = await mt.order_send(buy_order)
res2 = await mt.order_send(sell_order)
result1 = Result(result=OrderSendResult(**res1._asdict()), parameters=parameters)
result2 = Result(result=OrderSendResult(**res2._asdict()), parameters=parameters)
return result1, result2
async def test_concurrent_csv_saves(self, order_results):
"""Test concurrent CSV saves are thread-safe."""
res1, res2 = order_results
await asyncio.gather(
res1.save(trade_record_mode="csv"),
res2.save(trade_record_mode="csv")
)
file = res1.config.records_dir / f"{res1.name}.csv"
assert file.exists()
async def test_concurrent_json_saves(self, order_results):
"""Test concurrent JSON saves are thread-safe."""
res1, res2 = order_results
await asyncio.gather(
res1.save(trade_record_mode="json"),
res2.save(trade_record_mode="json")
)
file = res1.config.records_dir / f"{res1.name}.json"
assert file.exists()
class TestResultIntegration:
"""Integration tests for Result with live trading."""
@pytest.fixture(scope="class")
def parameters(self):
"""Create test parameters."""
return {"name": "test_integration", "symbol": "BTCUSD", "ema": 20, "rsi": 14}
@pytest.fixture(scope="function")
async def order_results(self, mt, buy_order, sell_order, parameters):
"""Create orders and return Result instances."""
res1 = await mt.order_send(buy_order)
res2 = await mt.order_send(sell_order)
result1 = Result(result=OrderSendResult(**res1._asdict()), parameters=parameters)
result2 = Result(result=OrderSendResult(**res2._asdict()), parameters=parameters)
return result1, result2
async def test_full_workflow_csv(self, order_results):
"""Test complete CSV workflow."""
res1, res2 = order_results
# Get data
data1 = res1.get_data()
data2 = res2.get_data()
assert "deal" in data1
assert "deal" in data2
# Save both
await asyncio.gather(res1.save(trade_record_mode="csv"), res2.save(trade_record_mode="csv"))
# Verify file exists with data
file = res1.config.records_dir / f"{res1.name}.csv"
assert file.exists()
with file.open("r") as fh:
reader = list(csv.DictReader(fh))
assert len(reader) >= 2
async def test_full_workflow_json(self, order_results):
"""Test complete JSON workflow."""
res1, res2 = order_results
# Save both
await asyncio.gather(res1.save(trade_record_mode="json"), res2.save(trade_record_mode="json"))
# Verify file exists with data
file = res1.config.records_dir / f"{res1.name}.json"
assert file.exists()
with file.open("r") as fh:
data = json.load(fh)
assert isinstance(data, list)
assert len(data) >= 2
class TestResultEdgeCases:
"""Test edge cases and boundary conditions."""
@pytest.fixture(scope="class")
def mock_order_result(self):
"""Create a mock OrderSendResult with TradeRequest."""
request = TradeRequest(
action=1,
type=0,
order=0,
symbol="EURUSD",
volume=0.01,
sl=1.0800,
tp=1.0900,
price=1.0850,
deviation=10,
stop_limit=0,
type_time=0,
type_filling=0,
expiration=0,
position=0,
position_by=0,
comment="",
magic=0
)
osr = OrderSendResult(
retcode=10009,
deal=12345,
order=67890,
volume=0.01,
price=1.0850,
bid=1.0849,
ask=1.0851,
comment="",
request_id=0,
retcode_external=0,
)
osr.request = request
return osr
def test_empty_parameters(self, mock_order_result):
"""Test Result with empty parameters dict."""
result = Result(result=mock_order_result, parameters={})
assert result.parameters == {}
assert result.name == "Trades"
def test_none_parameters(self, mock_order_result):
"""Test Result with None parameters."""
result = Result(result=mock_order_result, parameters=None)
assert result.parameters == {}
def test_empty_name(self, mock_order_result):
"""Test Result with empty name string."""
result = Result(result=mock_order_result, name="")
assert result.name == "Trades"
def test_special_characters_in_name(self, mock_order_result):
"""Test Result with special characters in name."""
result = Result(result=mock_order_result, name="Test_Strategy-123")
assert result.name == "Test_Strategy-123"
def test_large_parameters_dict(self, mock_order_result):
"""Test Result with large parameters dictionary."""
large_params = {f"param_{i}": i for i in range(100)}
large_params["name"] = "LargeParams"
result = Result(result=mock_order_result, parameters=large_params)
assert len(result.parameters) == 101
def test_nested_parameters(self, mock_order_result):
"""Test Result with nested parameters."""
nested_params = {
"name": "Nested",
"symbol": "EURUSD",
"settings": {"ema": [10, 20, 50], "rsi": 14}
}
result = Result(result=mock_order_result, parameters=nested_params)
assert result.parameters["settings"]["ema"] == [10, 20, 50]
def test_multiple_extra_params(self, mock_order_result):
"""Test Result with multiple extra parameters."""
result = Result(
result=mock_order_result,
time=1705312800000,
expected_profit=15.0,
custom1="value1",
custom2=42,
custom3=[1, 2, 3]
)
assert len(result.extra_params) == 5
def test_get_data_preserves_order_values(self, mock_order_result):
"""Test get_data preserves original order values."""
result = Result(result=mock_order_result)
data = result.get_data()
assert data["deal"] == 12345
assert data["order"] == 67890
assert data["volume"] == 0.01
assert data["price"] == 1.0850
def test_get_data_excludes_none_profit_loss(self, mock_order_result):
"""Test get_data excludes OrderSendResult fields with None values."""
# OrderSendResult has profit=None and loss=None by default
# get_dict filters out None values, so they should not appear
result = Result(result=mock_order_result)
res_dict = mock_order_result.get_dict(
exclude={"retcode", "comment", "retcode_external", "request_id", "request"}
)
# profit and loss have None defaults and should be filtered out by get_dict
assert "loss" not in res_dict