mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-17 13:58:15 +00:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user