Add integration test for Result and TradeRecords classes

rename unittests to unit
change TradeRecords.get_csv_records and TradeRecord.get_json_records methods to non-async.
This commit is contained in:
Ichinga Samuel
2024-10-09 00:16:40 +01:00
parent 76a947640f
commit 06061ec917
18 changed files with 116 additions and 15 deletions
+3 -2
View File
@@ -62,7 +62,8 @@ class Config:
use_terminal_for_backtesting: bool
_defaults = {"timeout": 60000, "record_trades": True, "trade_record_mode": "csv", "mode": "live",
'filename': "aiomql.json", "records_dir_name": "trade_records", "backtest_dir_name": "backtester",
"use_terminal_for_backtesting": True, 'path': '', 'login': 0, 'password': '', 'server': ''}
"use_terminal_for_backtesting": True, 'path': '', 'login': 0, 'password': '',
'server': '', 'records_dir': None}
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
@@ -161,7 +162,7 @@ class Config:
if self.path:
self.path = self.root / self.path if not Path(self.path).resolve().exists() else self.path
if self.record_trades and not hasattr(self, "records_dir"):
if self.record_trades and (hasattr(self, "records_dir") is False or self.records_dir is None):
self.records_dir = self.root / self.records_dir_name
self.records_dir.mkdir(parents=True, exist_ok=True)
+16 -10
View File
@@ -7,6 +7,8 @@ import csv
import logging
from typing import Iterable
from MetaTrader5 import TradePosition
from ..core.config import Config
from ..core.meta_trader import MetaTrader
from ..core.meta_backtester import MetaBackTester
@@ -24,6 +26,7 @@ class TradeRecords:
"""
config: Config
mt5: MetaTrader | MetaBackTester
positions: list[TradePosition] | None = None
def __init__(self, *, records_dir: Path | str = ''):
"""Initialize the Records class. The main method of this class is update_records which you should call to update
@@ -33,10 +36,10 @@ class TradeRecords:
records_dir (Path): Absolute path to directory containing record of placed trades.
"""
self.config = Config()
self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaBackTester()
self.mt5 = MetaTrader() if self.config.mode != 'backtest' else MetaBackTester()
self.records_dir = records_dir or self.config.records_dir
async def get_csv_records(self):
def get_csv_records(self):
"""Get trade records saved as csv from records_dir folder
Yields:
@@ -46,11 +49,11 @@ class TradeRecords:
if file.is_file() and file.name.endswith('.csv'):
yield file
async def get_json_records(self):
def get_json_records(self):
"""Get trade records from records_dir folder
Yields:
files: Trade record files
files (Path): Trade record files
"""
for file in self.records_dir.iterdir():
if file.is_file() and file.name.endswith('.json'):
@@ -68,7 +71,7 @@ class TradeRecords:
rows = [row for row in reader]
rows = await self.update_rows(rows=rows)
with open(file, mode='w', newline='') as fw: # type: SupportsWrite[str]
with open(file, mode='w', newline='') as fw:
writer = csv.DictWriter(fw, fieldnames=reader.fieldnames, extrasaction='ignore', restval=None)
writer.writeheader()
writer.writerows(rows)
@@ -81,12 +84,12 @@ class TradeRecords:
file: Trade record file in csv format
"""
try:
with open(file, mode='r') as fh: # type: SupportsRead[str | bytes]
with open(file, mode='r') as fh:
data = json.load(fh)
rows = [row for row in data]
rows = await self.update_rows(rows=rows)
with open(file, mode='w') as fh: # type: SupportsWrite[str]
with open(file, mode='w') as fh:
json.dump(rows, fh, indent=2)
except Exception as err:
logger.error(f'Error: {err}. Unable to read and update json trade records')
@@ -102,11 +105,13 @@ class TradeRecords:
"""
try:
order = int(row['order'])
positions = self.positions or await self.mt5.positions_get()
position_ids = [position.ticket for position in positions]
deals = await self.mt5.history_deals_get(position=order)
if not deals or len(deals) <= 1:
return row
deals = [deal for deal in deals if (deal.order != deal.position_id and deal.position_id == order
and deal.entry == 1)]
and deal.entry == 1 and deal.position_id not in position_ids)]
deals.sort(key=lambda deal: deal.time_msc)
deal = deals[-1]
row.update(actual_profit=deal.profit, win=deal.profit > 0, closed=True)
@@ -124,6 +129,7 @@ class TradeRecords:
Returns:
list[dict]: A list of dictionaries with the actual profit and win status.
"""
self.positions = await self.mt5.positions_get()
closed, unclosed = [], []
for row in rows:
closed_ = row.get('closed', False)
@@ -137,12 +143,12 @@ class TradeRecords:
async def update_csv_records(self):
"""Update csv trade records in the records_dir folder."""
records = [self.read_update_csv(file=record) async for record in self.get_csv_records()]
records = [self.read_update_csv(file=record) for record in self.get_csv_records()]
await asyncio.gather(*records)
async def update_json_records(self):
"""Update json trade records in the records_dir folder."""
records = [self.read_update_json(file=record) async for record in self.get_json_records()]
records = [self.read_update_json(file=record) for record in self.get_json_records()]
await asyncio.gather(*records)
async def update_csv_record(self, *, file: Path | str):
+1 -1
View File
@@ -77,7 +77,7 @@ async def buy_order(mt):
'type': mt.ORDER_TYPE_BUY, 'price': sym_info.ask, 'sl': sl, 'tp': tp}
@pytest.fixture(scope='class')
async def make_orders(mt):
async def make_buy_sell_orders(mt):
sym = 'BTCUSD'
sym_info = await mt.symbol_info(sym)
dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point
+94
View File
@@ -0,0 +1,94 @@
import asyncio
import json
from csv import DictReader
import pytest
from aiomql.lib.result import Result
from aiomql.core.models import OrderSendResult
from aiomql.lib.trade_records import TradeRecords
from aiomql.lib.positions import Positions
class TestRecordsAndResults:
@classmethod
def setup_class(cls):
cls.trade_records = TradeRecords()
@pytest.fixture(scope='class')
async def buy(self, mt):
sym = 'BTCUSD'
sym_info = await mt.symbol_info(sym)
dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point
sl = sym_info.ask - dsl
tp = sym_info.ask + dsl
return {'action': mt.TRADE_ACTION_DEAL, 'symbol': sym, 'volume': sym_info.volume_min,
'type': mt.ORDER_TYPE_BUY, 'price': sym_info.ask, 'sl': sl, 'tp': tp}
@pytest.fixture(scope='class')
async def sell(self, mt):
sym = 'BTCUSD'
sym_info = await mt.symbol_info(sym)
return {'action': mt.TRADE_ACTION_DEAL, 'symbol': sym, 'volume': sym_info.volume_min,
'type': mt.ORDER_TYPE_SELL, 'price': sym_info.bid}
@pytest.fixture(scope='class', autouse=True)
async def setup(self, sell, buy, mt):
buy_res = await mt.order_send(buy)
buy_res_2 = await mt.order_send(buy)
sell_res = await mt.order_send(sell)
sell_res_2 = await mt.order_send(sell)
buy_res = Result(result=OrderSendResult(**buy_res._asdict()), name='test_result')
sell_res = Result(result=OrderSendResult(**sell_res._asdict()), name='test_result')
sell_res_2 = Result(result=OrderSendResult(**sell_res_2._asdict()), name='test_result')
buy_res_2 = Result(result=OrderSendResult(**buy_res_2._asdict()), name='test_result')
await asyncio.gather(buy_res.save(), sell_res.save(), buy_res_2.save(trade_record_mode='json'),
sell_res_2.save(trade_record_mode='json'))
await Positions().close_all()
def test_records_dir(self):
records_dir = self.trade_records.records_dir
assert records_dir.is_dir()
recs = list(records_dir.iterdir())
assert len(recs) >= 2
csvs, jsons = [], []
matched_recs = list(records_dir.glob("test_result.*"))
assert len(matched_recs) == 2
for rec in matched_recs:
if rec.match("test_result.json"):
jsons.append(rec)
elif rec.match("test_result.csv"):
csvs.append(rec)
else:
continue
assert len(csvs) == 1
assert len(jsons) == 1
async def test_json_records(self):
json_records = self.trade_records.get_json_records()
matched_recs = [record for record in json_records if record.match('test_result.json')]
assert len(matched_recs) == 1
record = matched_recs[0]
record_data = json.load(record.open())
assert isinstance(record_data, list)
assert len(record_data) == 2
is_open = [data['closed'] is False for data in record_data]
assert all(is_open)
await self.trade_records.update_json_records()
is_close = [data['closed'] is True for data in record_data]
assert len(is_close) == 2
async def test_csv_records(self):
csv_records = self.trade_records.get_csv_records()
matched_recs = [record for record in csv_records if record.match('test_result.csv')]
assert len(matched_recs) == 1
record = matched_recs[0]
record_data = DictReader(record.open())
record_data = [row for row in record_data]
assert isinstance(record_data, list)
assert len(record_data) == 2
is_open = [data['closed'].title() == 'False' for data in record_data]
assert all(is_open)
await self.trade_records.update_json_records()
is_close = [data['closed'].title() == 'True' for data in record_data]
assert len(is_close) == 2
@@ -5,7 +5,7 @@ from aiomql.lib.history import History
class TestHistory:
@pytest.fixture(scope='class', autouse=True)
async def init(self, make_orders):
async def init(self, make_buy_sell_orders):
await self.history.init()
@classmethod
@@ -5,7 +5,7 @@ from aiomql.lib.positions import Positions
class TestPositions:
@pytest.fixture(scope='class', autouse=True)
async def init(self, make_orders):
async def init(self, make_buy_sell_orders):
await self.positions.get_positions()
@classmethod